From 7c542977a67c407afa88288ff87294404b5e6c80 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Fri, 30 Jan 2026 22:38:29 -0500 Subject: [PATCH] feat: ESPI 4.0 Schema Compliance - Phase 21: ServiceSupplier Complete Implementation This phase implements full ESPI 4.0 compliance for ServiceSupplier and refactors all Organisation phone number handling from PhoneNumberEntity table to embedded TelephoneNumber fields. Key Changes: - Created ServiceSupplierDto, OrganisationDto, TelephoneNumberDto with JAXB annotations - Implemented ServiceSupplierMapper, OrganisationMapper, TelephoneNumberMapper - Fixed SupplierKind enum to match XSD (6 values: UTILITY, RETAILER, OTHER, LSE, MDMA, MSP) - Refactored Organisation to use embedded phone1/phone2 (TelephoneNumber) - Deleted PhoneNumberEntity, OrganisationRole, PhoneNumberService (no longer needed) - Updated ServiceSupplierEntity and CustomerEntity with phone @AttributeOverride - Optimized TelephoneNumber column sizes to prevent MySQL row size limit - Updated V3 Flyway migration with embedded phone columns - Resolved Spring bean conflicts (removed BaseMapperUtils inheritance) - Removed legacy usagePointId field from ElectricPowerQualitySummaryDto - Added 24 new DTO tests (ServiceSupplierDtoTest, OrganisationDtoTest, TelephoneNumberDtoTest) Test Results: - Total: 760 tests (all passing) - Integration: PostgreSQL (2/2), H2 (3/3), MySQL (2/2) Related Issue: #28 (Phase 21) Co-Authored-By: Claude Sonnet 4.5 --- ...21_SERVICE_SUPPLIER_IMPLEMENTATION_PLAN.md | 669 ++++++++++++++++++ .../customer/common/TelephoneNumber.java | 16 +- .../entity/CustomerAccountEntity.java | 16 + .../customer/entity/CustomerEntity.java | 27 +- .../domain/customer/entity/Organisation.java | 14 +- .../customer/entity/OrganisationRole.java | 63 -- .../customer/entity/PhoneNumberEntity.java | 164 ----- .../entity/ServiceSupplierEntity.java | 27 +- .../domain/customer/enums/SupplierKind.java | 38 +- .../common/dto/atom/CustomerAtomEntryDto.java | 5 +- .../dto/customer/CustomerAccountDto.java | 2 +- .../espi/common/dto/customer/CustomerDto.java | 73 -- .../common/dto/customer/OrganisationDto.java | 92 +++ .../dto/customer/ServiceLocationDto.java | 4 +- .../dto/customer/ServiceSupplierDto.java | 81 +++ .../dto/customer/TelephoneNumberDto.java | 106 +++ .../usage/ElectricPowerQualitySummaryDto.java | 8 - .../mapper/customer/CustomerMapper.java | 150 +--- .../mapper/customer/OrganisationMapper.java | 27 +- .../customer/ServiceLocationMapper.java | 36 +- .../customer/ServiceSupplierMapper.java | 97 +++ .../customer/TelephoneNumberMapper.java | 47 ++ .../ElectricPowerQualitySummaryMapper.java | 2 - .../common/service/PhoneNumberService.java | 194 ----- .../service/impl/DtoExportServiceImpl.java | 4 + .../V3__Create_additiional_Base_Tables.sql | 86 ++- .../dto/customer/CustomerAccountDtoTest.java | 2 +- .../customer/CustomerDtoMarshallingTest.java | 6 +- .../common/dto/customer/CustomerDtoTest.java | 10 +- .../dto/customer/OrganisationDtoTest.java | 219 ++++++ .../dto/customer/ServiceLocationDtoTest.java | 4 +- .../dto/customer/ServiceSupplierDtoTest.java | 421 +++++++++++ .../dto/customer/TelephoneNumberDtoTest.java | 201 ++++++ .../ServiceLocationRepositoryTest.java | 12 - 34 files changed, 2136 insertions(+), 787 deletions(-) create mode 100644 openespi-common/PHASE_21_SERVICE_SUPPLIER_IMPLEMENTATION_PLAN.md delete mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java delete mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDto.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDto.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDto.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceSupplierMapper.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/TelephoneNumberMapper.java delete mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/PhoneNumberService.java create mode 100644 openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDtoTest.java create mode 100644 openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDtoTest.java create mode 100644 openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDtoTest.java diff --git a/openespi-common/PHASE_21_SERVICE_SUPPLIER_IMPLEMENTATION_PLAN.md b/openespi-common/PHASE_21_SERVICE_SUPPLIER_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..a5f32d65 --- /dev/null +++ b/openespi-common/PHASE_21_SERVICE_SUPPLIER_IMPLEMENTATION_PLAN.md @@ -0,0 +1,669 @@ +# Phase 21: ServiceSupplier & Organisation Phone Refactoring - ESPI 4.0 Schema Compliance + +## Overview +Implement full ESPI 4.0 schema compliance for ServiceSupplier AND refactor all Organisation phone number handling from PhoneNumberEntity table to embedded TelephoneNumber fields. This comprehensive update affects ServiceSupplier, Customer, and all related infrastructure to prevent CI/CD issues when removing PhoneNumberEntity. + +**Related Issue**: #28 - ESPI 4.0 Schema Compliance (Phase 21: ServiceSupplier) + +**IMPORTANT**: Issue #28 tracks the multi-phase ESPI 4.0 schema compliance effort. This plan implements Phase 21 only. **DO NOT close Issue #28** when Phase 21 is complete - additional phases (Phase 22+) remain to be implemented. + +**Scope Expansion Rationale**: +- SupplierKind enum does NOT match XSD (wrong values, wrong sequence - must be fixed) +- ServiceSupplierEntity uses PhoneNumberEntity (must be refactored) +- CustomerEntity also uses PhoneNumberEntity (must be refactored to prevent build failure) +- PhoneNumberService depends on PhoneNumberEntity (must be deleted) +- CustomerMapper has PhoneNumberEntity logic (must be updated) +- V3 Flyway migration creates phone_numbers table (must be removed) +- Completing all phone refactoring in one phase prevents partial implementation issues + +**Critical Discovery**: +- Current SupplierKind enum has wrong values (RETAIL, GENERATION, TRANSMISSION, DISTRIBUTION) +- XSD defines: UTILITY, RETAILER, OTHER, LSE, MDMA, MSP (6 values, not 5) +- ESPI standard uses **ordinal values** (0-5), so sequence is critical +- Enum must be fixed to match XSD exactly for ESPI Sandbox/CMD Certification compatibility + +## XSD Structure + +**ServiceSupplier extends IdentifiedObject** (customer.xsd lines 1159-1186): + +### ServiceSupplier Fields (4 fields) +1. **organisation** (Organisation) - Embedded organisation details +2. **kind** (SupplierKind enum) - Type of supplier + - Per XSD (lines 2231-2271): UTILITY(0), RETAILER(1), OTHER(2), LSE(3), MDMA(4), MSP(5) + - ESPI serializes using ordinal values (0-5), not string values + - Sequence is critical for correct serialization +3. **issuerIdentificationNumber** (String256) - Unique supplier identifier +4. **effectiveDate** (TimeType -> Long) - Date supplier became effective + +### Organisation Structure (customer.xsd lines 1089-1125) +Organisation extends IdentifiedObject in XSD but is @Embeddable in implementation: +- **organisationName** (String256) +- **streetAddress** (StreetAddress) +- **postalAddress** (StreetAddress) +- **phone1** (TelephoneNumber) - All 8 TelephoneNumber fields +- **phone2** (TelephoneNumber) - All 8 TelephoneNumber fields +- **electronicAddress** (ElectronicAddress) + +### TelephoneNumber Structure (customer.xsd lines 1428-1478) +TelephoneNumber extends Object (NOT IdentifiedObject) - 8 fields: +- countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone + +### Architecture Decision +- **OrganisationRole**: Unused wrapper - will be removed +- **PhoneNumberEntity**: Polymorphic table pattern - will be removed +- **Embedded TelephoneNumber**: XSD-compliant, type-safe, performant - will be implemented + +## Tasks + +### Phase A: Verification and Cleanup + +#### Task A1: Verify and Fix SupplierKind Enum +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/SupplierKind.java` + +**Current Issues Found**: +1. ❌ **Wrong values**: RETAIL, GENERATION, TRANSMISSION, DISTRIBUTION are incorrect +2. ❌ **Missing values**: LSE, MDMA, MSP from XSD +3. ❌ **Wrong sequence**: OTHER is in position 6 instead of position 3 + +**XSD Definition** (customer.xsd lines 2231-2271): +```xml + + + + + + +``` + +**ESPI Serialization Note**: +- ESPI standard (Sandbox and CMD Certification Platform) emits **ordinal values** (0, 1, 2, 3, 4, 5) +- NOT string values ("utility", "retailer", etc.) +- Sequence is CRITICAL as ordinal position determines serialized value + +**Required Changes**: + +Replace entire enum with XSD-compliant values in correct sequence: + +```java +package org.greenbuttonalliance.espi.common.domain.customer.enums; + +/** + * Kind of supplier based on ESPI 4.0 customer.xsd specification. + * + * Per customer.xsd lines 2231-2271. + * CRITICAL: Sequence must match XSD exactly as ESPI uses ordinal values (0-5) for serialization. + * + * Ordinal mapping: + * 0 = UTILITY + * 1 = RETAILER + * 2 = OTHER + * 3 = LSE + * 4 = MDMA + * 5 = MSP + */ +public enum SupplierKind { + /** + * Entity that delivers the service to the customer. + * Ordinal: 0 + */ + UTILITY, + + /** + * Entity that sells the service, but does not deliver to the customer. + * Applies to the deregulated markets. + * Ordinal: 1 + */ + RETAILER, + + /** + * Other kind of supplier. + * Ordinal: 2 + */ + OTHER, + + /** + * [extension] Load Serving Entity + * Ordinal: 3 + */ + LSE, + + /** + * [extension] Meter Data Management Agent + * Ordinal: 4 + */ + MDMA, + + /** + * [extension] Meter Service Provider + * Ordinal: 5 + */ + MSP +} +``` + +**Verification Checklist**: +- ✅ Location: `customer/enums` directory (already correct) +- ✅ Sequence: Matches XSD exactly (UTILITY=0, RETAILER=1, OTHER=2, LSE=3, MDMA=4, MSP=5) +- ✅ Values: Six enum constants matching XSD +- ✅ Serialization: Uses ordinal values per ESPI standard +- ✅ No custom string values or @JsonValue annotations + +**Impact**: +- Existing ServiceSupplierEntity uses this enum (must verify after fix) +- XML/JSON serialization will use ordinals 0-5 +- Any existing data with old enum values will need migration (but this is dev system) + +#### Task A2: Delete OrganisationRole Class +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java` + +**Action**: Delete this file completely. + +**Rationale**: Unused wrapper with no additional elements beyond Organisation. + +#### Task A3: Delete PhoneNumberEntity Class +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java` + +**Action**: Delete this file completely. + +**Rationale**: Being replaced with embedded TelephoneNumber in Organisation. Polymorphic table pattern no longer needed. + +#### Task A4: Delete PhoneNumberService +**File**: `src/main/java/org/greenbuttonalliance/espi/common/service/PhoneNumberService.java` + +**Action**: Delete this file completely. + +**Rationale**: Service for managing PhoneNumberEntity relationships no longer needed with embedded approach. + +### Phase B: Organisation and Entity Updates + +#### Task B1: Update Organisation to Include phone1/phone2 +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java` + +**Changes Required**: +1. Add phone1 and phone2 embedded fields using existing TelephoneNumber class +2. Remove comment about phone numbers being managed separately (lines 62-63) +3. Update JavaDoc to reflect XSD-compliant structure + +**New Fields to Add**: +```java +/** + * Primary phone number for this organisation. + */ +@Embedded +private TelephoneNumber phone1; + +/** + * Secondary phone number for this organisation. + */ +@Embedded +private TelephoneNumber phone2; +``` + +**Note**: Column name overrides applied at entity level (ServiceSupplierEntity, CustomerEntity), not in Organisation. + +#### Task B2: Update ServiceSupplierEntity for XSD Compliance +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java` + +**Changes Required**: + +1. **Remove PhoneNumberEntity relationship**: + - Delete `phoneNumbers` field (lines 94-101) + - Remove `@OneToMany`, `@JoinColumn`, `@SQLRestriction` annotations + - Remove import: `org.hibernate.annotations.SQLRestriction` + +2. **Update @AttributeOverrides to JDK 25 pattern** (convert from wrapper): + - Convert from `@AttributeOverrides({...})` to individual `@AttributeOverride` + +3. **Add phone field overrides** (16 new overrides for 2 phones × 8 fields each): +```java +// Phone1 overrides +@AttributeOverride(name = "phone1.countryCode", column = @Column(name = "supplier_phone1_country_code")) +@AttributeOverride(name = "phone1.areaCode", column = @Column(name = "supplier_phone1_area_code")) +@AttributeOverride(name = "phone1.cityCode", column = @Column(name = "supplier_phone1_city_code")) +@AttributeOverride(name = "phone1.localNumber", column = @Column(name = "supplier_phone1_local_number")) +@AttributeOverride(name = "phone1.ext", column = @Column(name = "supplier_phone1_ext")) +@AttributeOverride(name = "phone1.dialOut", column = @Column(name = "supplier_phone1_dial_out")) +@AttributeOverride(name = "phone1.internationalPrefix", column = @Column(name = "supplier_phone1_international_prefix")) +@AttributeOverride(name = "phone1.ituPhone", column = @Column(name = "supplier_phone1_itu_phone")) + +// Phone2 overrides +@AttributeOverride(name = "phone2.countryCode", column = @Column(name = "supplier_phone2_country_code")) +@AttributeOverride(name = "phone2.areaCode", column = @Column(name = "supplier_phone2_area_code")) +@AttributeOverride(name = "phone2.cityCode", column = @Column(name = "supplier_phone2_city_code")) +@AttributeOverride(name = "phone2.localNumber", column = @Column(name = "supplier_phone2_local_number")) +@AttributeOverride(name = "phone2.ext", column = @Column(name = "supplier_phone2_ext")) +@AttributeOverride(name = "phone2.dialOut", column = @Column(name = "supplier_phone2_dial_out")) +@AttributeOverride(name = "phone2.internationalPrefix", column = @Column(name = "supplier_phone2_international_prefix")) +@AttributeOverride(name = "phone2.ituPhone", column = @Column(name = "supplier_phone2_itu_phone")) +``` + +4. **Update toString() method** to remove phoneNumbers reference + +5. **Add comprehensive JavaDoc** + +**Total @AttributeOverride count**: ~34 overrides + +#### Task B3: Update CustomerEntity for XSD Compliance +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java` + +**Changes Required**: + +1. **Remove PhoneNumberEntity relationship**: + - Delete `phoneNumbers` field (lines 162-169) + - Remove `@OneToMany`, `@JoinColumn`, `@SQLRestriction` annotations + +2. **Update @AttributeOverrides to JDK 25 pattern** (convert from wrapper): + - Convert from `@AttributeOverrides({...})` to individual `@AttributeOverride` + +3. **Add phone field overrides** (16 new overrides for 2 phones × 8 fields each): +```java +// Phone1 overrides +@AttributeOverride(name = "phone1.countryCode", column = @Column(name = "customer_phone1_country_code")) +@AttributeOverride(name = "phone1.areaCode", column = @Column(name = "customer_phone1_area_code")) +@AttributeOverride(name = "phone1.cityCode", column = @Column(name = "customer_phone1_city_code")) +@AttributeOverride(name = "phone1.localNumber", column = @Column(name = "customer_phone1_local_number")) +@AttributeOverride(name = "phone1.ext", column = @Column(name = "customer_phone1_ext")) +@AttributeOverride(name = "phone1.dialOut", column = @Column(name = "customer_phone1_dial_out")) +@AttributeOverride(name = "phone1.internationalPrefix", column = @Column(name = "customer_phone1_international_prefix")) +@AttributeOverride(name = "phone1.ituPhone", column = @Column(name = "customer_phone1_itu_phone")) + +// Phone2 overrides (similar pattern with customer_phone2_* prefix) +``` + +4. **Update toString() method** to remove phoneNumbers reference + +**Total @AttributeOverride count**: ~35 overrides + +### Phase C: DTO Implementation + +#### Task C1: Create ServiceSupplierDto +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDto.java` + +**Requirements**: +- Use JAXB annotations (NOT Jackson) +- Include ONLY 4 ServiceSupplier-specific fields +- NO IdentifiedObject fields (handled by AtomEntryDto) +- Namespace: `http://naesb.org/espi/customer` + +**Structure**: [Same as original plan - full DTO structure with JAXB annotations] + +#### Task C2: Create/Update OrganisationDto +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDto.java` + +**Requirements**: +- Include all 6 fields: organisationName, streetAddress, postalAddress, phone1, phone2, electronicAddress +- Use existing TelephoneNumberDto for phone1/phone2 + +**Structure**: [Same as original plan] + +#### Task C3: Create TelephoneNumberDto +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDto.java` + +**Requirements**: +- Use JAXB annotations +- Include all 8 fields from XSD + +**Structure**: [Same as original plan] + +#### Task C4: Verify Supporting DTOs Exist +- StreetAddressDto +- ElectronicAddressDto + +### Phase D: Mapper Updates + +#### Task D1: Implement ServiceSupplierMapper +**File**: `src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceSupplierMapper.java` + +**Changes**: +- Map ONLY 4 ServiceSupplier-specific fields +- Direct field-to-field mapping: `organisation.phone1 → dto.phone1` +- UUID v5 generation from issuerIdentificationNumber +- OffsetDateTime ↔ Long conversion for effectiveDate + +**Structure**: [Same as original plan] + +#### Task D2: Create OrganisationMapper +**File**: `src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/OrganisationMapper.java` + +**Structure**: [Same as original plan] + +#### Task D3: Update CustomerMapper +**File**: `src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java` + +**Changes Required**: + +1. **Remove PhoneNumberEntity imports and logic**: + - Remove import: `org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity` + - Remove `@Mapping(target = "phoneNumbers", ignore = true)` (line 80) + - Delete `extractPhoneByType()` method (lines 206-220) + - Update `mapOrganisationToDto()` to use direct phone1/phone2 instead of extracting from list + +2. **Update mapOrganisationToDto()** method: +```java +@Named("mapOrganisationToDto") +default CustomerDto.OrganisationDto mapOrganisationToDto(CustomerEntity entity) { + if (entity == null || entity.getOrganisation() == null) { + return null; + } + + Organisation org = entity.getOrganisation(); + + // Direct mapping from embedded TelephoneNumber + CustomerDto.TelephoneNumberDto phone1 = telephoneNumberToDto(org.getPhone1()); + CustomerDto.TelephoneNumberDto phone2 = telephoneNumberToDto(org.getPhone2()); + + return new CustomerDto.OrganisationDto( + mapStreetAddressToDto(org.getStreetAddress()), + mapStreetAddressToDto(org.getPostalAddress()), + phone1, + phone2, + mapElectronicAddressToDto(org.getElectronicAddress()), + org.getOrganisationName() + ); +} + +// Add TelephoneNumber mapping method +default CustomerDto.TelephoneNumberDto telephoneNumberToDto(TelephoneNumber tel) { + if (tel == null) return null; + return new CustomerDto.TelephoneNumberDto( + tel.getCountryCode(), + tel.getAreaCode(), + tel.getCityCode(), + tel.getLocalNumber(), + tel.getExt(), + tel.getDialOut(), + tel.getInternationalPrefix(), + tel.getItuPhone() + ); +} +``` + +3. **Remove comment about PhoneNumberEntity** (lines 95-96, 125, 205) + +### Phase E: Database Migration + +#### Task E1: Update V3 Flyway Migration Script +**File**: `src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Purpose**: Update CREATE TABLE statements to include embedded phone columns; remove phone_numbers table + +**Changes Required**: + +1. **Update service_suppliers CREATE TABLE** - Add 16 phone columns to existing CREATE TABLE statement: +```sql +CREATE TABLE service_suppliers ( + -- ... existing columns ... + + -- Embedded TelephoneNumber fields for Organisation.phone1 + supplier_phone1_country_code VARCHAR(256), + supplier_phone1_area_code VARCHAR(256), + supplier_phone1_city_code VARCHAR(256), + supplier_phone1_local_number VARCHAR(256), + supplier_phone1_ext VARCHAR(256), + supplier_phone1_dial_out VARCHAR(256), + supplier_phone1_international_prefix VARCHAR(256), + supplier_phone1_itu_phone VARCHAR(256), + + -- Embedded TelephoneNumber fields for Organisation.phone2 + supplier_phone2_country_code VARCHAR(256), + supplier_phone2_area_code VARCHAR(256), + supplier_phone2_city_code VARCHAR(256), + supplier_phone2_local_number VARCHAR(256), + supplier_phone2_ext VARCHAR(256), + supplier_phone2_dial_out VARCHAR(256), + supplier_phone2_international_prefix VARCHAR(256), + supplier_phone2_itu_phone VARCHAR(256), + + -- ... rest of columns ... +); +``` + +2. **Update customers CREATE TABLE** - Add 16 phone columns to existing CREATE TABLE statement: +```sql +CREATE TABLE customers ( + -- ... existing columns ... + + -- Embedded TelephoneNumber fields for Organisation.phone1 + customer_phone1_country_code VARCHAR(256), + customer_phone1_area_code VARCHAR(256), + customer_phone1_city_code VARCHAR(256), + customer_phone1_local_number VARCHAR(256), + customer_phone1_ext VARCHAR(256), + customer_phone1_dial_out VARCHAR(256), + customer_phone1_international_prefix VARCHAR(256), + customer_phone1_itu_phone VARCHAR(256), + + -- Embedded TelephoneNumber fields for Organisation.phone2 + customer_phone2_country_code VARCHAR(256), + customer_phone2_area_code VARCHAR(256), + customer_phone2_city_code VARCHAR(256), + customer_phone2_local_number VARCHAR(256), + customer_phone2_ext VARCHAR(256), + customer_phone2_dial_out VARCHAR(256), + customer_phone2_international_prefix VARCHAR(256), + customer_phone2_itu_phone VARCHAR(256), + + -- ... rest of columns ... +); +``` + +3. **Remove phone_numbers table** (if CREATE TABLE phone_numbers exists in V3): + - Delete the entire CREATE TABLE phone_numbers statement + - Phone numbers are now embedded, not in separate table + +### Phase F: Test Data and Test Updates + +#### Task F1: Update TestDataBuilders +**File**: `src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java` + +**Changes**: + +1. **Update createValidServiceSupplier()** - Replace phoneNumbers collection with embedded phone1/phone2: +```java +public static ServiceSupplierEntity createValidServiceSupplier() { + // ... existing code ... + + // Phone1 (primary) + TelephoneNumber phone1 = new TelephoneNumber(); + phone1.setCountryCode("+1"); + phone1.setAreaCode(faker.number().digits(3)); + phone1.setLocalNumber(faker.number().digits(7)); + organisation.setPhone1(phone1); + + // Phone2 (secondary) + TelephoneNumber phone2 = new TelephoneNumber(); + phone2.setCountryCode("+1"); + phone2.setAreaCode(faker.number().digits(3)); + phone2.setLocalNumber(faker.number().digits(7)); + phone2.setExt(faker.number().digits(4)); + organisation.setPhone2(phone2); + + // Remove: supplier.setPhoneNumbers(...) + + return supplier; +} +``` + +2. **Update createValidCustomer()** - Replace phoneNumbers collection with embedded phone1/phone2 (same pattern) + +#### Task F2: Update ServiceLocationRepositoryTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java` + +**Changes**: +- Remove `createValidPhoneNumber()` helper method +- Update any tests that reference PhoneNumberEntity + +#### Task F3: Create ServiceSupplierRepositoryTest +[Same as original plan - 24 tests] + +#### Task F4: Create ServiceSupplier Integration Tests +[Same as original plan - MySQL and PostgreSQL - 10 tests each] + +#### Task F5: Create ServiceSupplierMapperTest +[Same as original plan - 10 tests] + +#### Task F6: Create OrganisationMapperTest +[Same as original plan - 8 tests] + +#### Task F7: Create ServiceSupplierDtoTest +[Same as original plan - 10 tests] + +#### Task F8: Create TelephoneNumberDtoTest +[Same as original plan - 4 tests] + +### Phase G: Service Integration + +#### Task G1: Update DtoExportService +**File**: `src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java` + +**Changes**: +- Add ServiceSupplierMapper injection +- Implement ServiceSupplier export method +- Ensure AtomEntryDto wrapping +- Add to batch export operations + +## Testing Strategy + +### Test Breakdown +- **Unit Tests (ServiceSupplier Repository)**: 24 tests +- **Integration Tests (ServiceSupplier MySQL)**: 10 tests +- **Integration Tests (ServiceSupplier PostgreSQL)**: 10 tests +- **Mapper Tests (ServiceSupplierMapper)**: 10 tests +- **Mapper Tests (OrganisationMapper)**: 8 tests +- **DTO Tests (ServiceSupplierDto)**: 10 tests +- **DTO Tests (TelephoneNumberDto)**: 4 tests + +**Total New Tests**: ~76 tests + +### Current Test Baseline +Based on previous phases: ~736 tests + +### Expected Test Count After Phase 21 +**Target**: ~812 tests (736 + 76) + +### Regression Testing +- All existing Customer tests must pass with embedded phone changes +- CustomerMapper tests must pass with updated phone logic +- Integration tests verify phone columns work on MySQL and PostgreSQL + +## Execution Checklist + +### Pre-Implementation Review +- [ ] Review XSD structures (ServiceSupplier, Organisation, TelephoneNumber, SupplierKind) +- [ ] Verify SupplierKind enum matches XSD (6 values in correct sequence) +- [ ] Identify all PhoneNumberEntity usages +- [ ] Confirm TelephoneNumber @Embeddable exists in customer/common +- [ ] Review V3 Flyway migration script structure + +### Phase A: Verification and Cleanup +- [ ] Verify and fix SupplierKind enum (6 values, correct sequence for ordinals) +- [ ] Delete OrganisationRole.java +- [ ] Delete PhoneNumberEntity.java +- [ ] Delete PhoneNumberService.java + +### Phase B: Entity Updates +- [ ] Update Organisation with phone1/phone2 fields +- [ ] Update ServiceSupplierEntity (remove phoneNumbers, add 16 phone @AttributeOverride, JDK 25 pattern) +- [ ] Update CustomerEntity (remove phoneNumbers, add 16 phone @AttributeOverride, JDK 25 pattern) + +### Phase C: DTO Implementation +- [ ] Create ServiceSupplierDto (JAXB, 4 fields) +- [ ] Create/update OrganisationDto (6 fields with phone1/phone2) +- [ ] Create TelephoneNumberDto (8 fields) +- [ ] Verify StreetAddressDto and ElectronicAddressDto exist + +### Phase D: Mapper Updates +- [ ] Implement ServiceSupplierMapper +- [ ] Create OrganisationMapper +- [ ] Update CustomerMapper (remove PhoneNumberEntity logic) + +### Phase E: Database Migration +- [ ] Update V3 service_suppliers CREATE TABLE (add 16 phone columns) +- [ ] Update V3 customers CREATE TABLE (add 16 phone columns) +- [ ] Remove phone_numbers CREATE TABLE from V3 + +### Phase F: Test Updates +- [ ] Update TestDataBuilders (ServiceSupplier and Customer methods) +- [ ] Update ServiceLocationRepositoryTest (remove PhoneNumberEntity helper) +- [ ] Create ServiceSupplierRepositoryTest (24 tests) +- [ ] Create ServiceSupplierMySQLIntegrationTest (10 tests) +- [ ] Create ServiceSupplierPostgreSQLIntegrationTest (10 tests) +- [ ] Create ServiceSupplierMapperTest (10 tests) +- [ ] Create OrganisationMapperTest (8 tests) +- [ ] Create ServiceSupplierDtoTest (10 tests) +- [ ] Create TelephoneNumberDtoTest (4 tests) + +### Phase G: Service Integration +- [ ] Update DtoExportService with ServiceSupplier support + +### Final Verification +- [ ] Run full test suite: `mvn clean test` +- [ ] Run integration tests: `mvn verify -Pintegration-tests` +- [ ] Verify test count: ~812 tests +- [ ] All tests passing on MySQL and PostgreSQL +- [ ] No PhoneNumberEntity references remain in codebase +- [ ] XML marshalling validates against customer.xsd + +### Documentation and Issue Tracking +- [ ] Update Issue #28 with Phase 21 completion status +- [ ] **DO NOT close Issue #28** - more phases remain (Phase 22+) +- [ ] Document SupplierKind enum fix in issue comment +- [ ] Document phone number architecture change (PhoneNumberEntity → embedded TelephoneNumber) +- [ ] Update CLAUDE.md if needed (phone number patterns, SupplierKind enum) + +## Success Criteria + +1. ✅ SupplierKind enum verified/fixed (6 values: UTILITY, RETAILER, OTHER, LSE, MDMA, MSP in correct sequence) +2. ✅ OrganisationRole removed +3. ✅ PhoneNumberEntity removed +4. ✅ PhoneNumberService removed +5. ✅ Organisation updated with phone1/phone2 +6. ✅ ServiceSupplierEntity refactored (embedded phones, JDK 25 pattern) +7. ✅ CustomerEntity refactored (embedded phones, JDK 25 pattern) +8. ✅ ServiceSupplierDto created (JAXB, 4 fields) +9. ✅ OrganisationDto includes phone1/phone2 +10. ✅ TelephoneNumberDto created (8 fields) +11. ✅ ServiceSupplierMapper implemented +12. ✅ OrganisationMapper implemented +13. ✅ CustomerMapper updated (no PhoneNumberEntity) +14. ✅ V3 Flyway script updated (no phone_numbers table, embedded columns in service_suppliers and customers) +15. ✅ All 76 new tests passing +16. ✅ Total test count: ~812 tests +17. ✅ No test regressions +18. ✅ XML validates against customer.xsd +19. ✅ Build succeeds with no PhoneNumberEntity references +20. ✅ SupplierKind enum ordinals match XSD sequence (ESPI Sandbox/CMD compatible) +21. ✅ Issue #28 updated with Phase 21 status (NOT closed - more phases remain) + +## Benefits + +### XSD Compliance +- ✅ Organisation matches XSD with phone1/phone2 +- ✅ TelephoneNumber structure matches XSD (8 fields) +- ✅ Direct entity → DTO mapping + +### Type Safety +- ✅ No string discriminators +- ✅ Compile-time checked relationships +- ✅ No polymorphic table risks + +### Performance +- ✅ Single table (no JOINs) +- ✅ All data in one row +- ✅ Simpler queries + +### Architecture +- ✅ Standard JPA embedded pattern +- ✅ Eliminates unnecessary abstractions (OrganisationRole, PhoneNumberEntity, PhoneNumberService) +- ✅ Cleaner codebase + +## References + +- **XSD**: `openespi-common/src/main/resources/schema/ESPI_4.0/customer.xsd` + - ServiceSupplier: lines 1159-1186 + - Organisation: lines 1089-1125 + - TelephoneNumber: lines 1428-1478 +- **Entity**: `ServiceSupplierEntity.java`, `CustomerEntity.java` +- **Embeddable**: `Organisation.java`, `TelephoneNumber.java` +- **Pattern Reference**: `CustomerDto.java`, `MeterDto.java` +- **Issue**: #28 (Phase 21: ServiceSupplier) diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java index 9ad62d2c..6150039a 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java @@ -47,28 +47,28 @@ @AllArgsConstructor public class TelephoneNumber implements Serializable { - @Column(name = "country_code", length = 256) + @Column(name = "country_code", length = 10) private String countryCode; - @Column(name = "area_code", length = 256) + @Column(name = "area_code", length = 10) private String areaCode; - @Column(name = "city_code", length = 256) + @Column(name = "city_code", length = 10) private String cityCode; - @Column(name = "local_number", length = 256) + @Column(name = "local_number", length = 30) private String localNumber; - @Column(name = "ext", length = 256) + @Column(name = "ext", length = 20) private String ext; - @Column(name = "dial_out", length = 256) + @Column(name = "dial_out", length = 10) private String dialOut; - @Column(name = "international_prefix", length = 256) + @Column(name = "international_prefix", length = 10) private String internationalPrefix; - @Column(name = "itu_phone", length = 256) + @Column(name = "itu_phone", length = 50) private String ituPhone; @Override diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java index 2f58847a..36276f3e 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java @@ -168,6 +168,22 @@ public class CustomerAccountEntity extends IdentifiedObject { @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "postal_state_or_province")) @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "postal_postal_code")) @AttributeOverride(name = "postalAddress.country", column = @Column(name = "postal_country")) + @AttributeOverride(name = "phone1.countryCode", column = @Column(name = "contact_phone1_country_code")) + @AttributeOverride(name = "phone1.areaCode", column = @Column(name = "contact_phone1_area_code")) + @AttributeOverride(name = "phone1.cityCode", column = @Column(name = "contact_phone1_city_code")) + @AttributeOverride(name = "phone1.localNumber", column = @Column(name = "contact_phone1_local_number")) + @AttributeOverride(name = "phone1.ext", column = @Column(name = "contact_phone1_ext")) + @AttributeOverride(name = "phone1.dialOut", column = @Column(name = "contact_phone1_dial_out")) + @AttributeOverride(name = "phone1.internationalPrefix", column = @Column(name = "contact_phone1_international_prefix")) + @AttributeOverride(name = "phone1.ituPhone", column = @Column(name = "contact_phone1_itu_phone")) + @AttributeOverride(name = "phone2.countryCode", column = @Column(name = "contact_phone2_country_code")) + @AttributeOverride(name = "phone2.areaCode", column = @Column(name = "contact_phone2_area_code")) + @AttributeOverride(name = "phone2.cityCode", column = @Column(name = "contact_phone2_city_code")) + @AttributeOverride(name = "phone2.localNumber", column = @Column(name = "contact_phone2_local_number")) + @AttributeOverride(name = "phone2.ext", column = @Column(name = "contact_phone2_ext")) + @AttributeOverride(name = "phone2.dialOut", column = @Column(name = "contact_phone2_dial_out")) + @AttributeOverride(name = "phone2.internationalPrefix", column = @Column(name = "contact_phone2_international_prefix")) + @AttributeOverride(name = "phone2.ituPhone", column = @Column(name = "contact_phone2_itu_phone")) @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "contact_lan")) @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "contact_mac")) @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "contact_email1")) diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java index 6b7661d4..2842e849 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java @@ -27,7 +27,6 @@ import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind; import org.greenbuttonalliance.espi.common.domain.usage.TimeConfigurationEntity; -import org.hibernate.annotations.SQLRestriction; import org.hibernate.proxy.HibernateProxy; import java.time.OffsetDateTime; @@ -69,6 +68,22 @@ public class CustomerEntity extends IdentifiedObject { @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "customer_postal_state_or_province")) @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "customer_postal_postal_code")) @AttributeOverride(name = "postalAddress.country", column = @Column(name = "customer_postal_country")) + @AttributeOverride(name = "phone1.countryCode", column = @Column(name = "customer_phone1_country_code")) + @AttributeOverride(name = "phone1.areaCode", column = @Column(name = "customer_phone1_area_code")) + @AttributeOverride(name = "phone1.cityCode", column = @Column(name = "customer_phone1_city_code")) + @AttributeOverride(name = "phone1.localNumber", column = @Column(name = "customer_phone1_local_number")) + @AttributeOverride(name = "phone1.ext", column = @Column(name = "customer_phone1_ext")) + @AttributeOverride(name = "phone1.dialOut", column = @Column(name = "customer_phone1_dial_out")) + @AttributeOverride(name = "phone1.internationalPrefix", column = @Column(name = "customer_phone1_international_prefix")) + @AttributeOverride(name = "phone1.ituPhone", column = @Column(name = "customer_phone1_itu_phone")) + @AttributeOverride(name = "phone2.countryCode", column = @Column(name = "customer_phone2_country_code")) + @AttributeOverride(name = "phone2.areaCode", column = @Column(name = "customer_phone2_area_code")) + @AttributeOverride(name = "phone2.cityCode", column = @Column(name = "customer_phone2_city_code")) + @AttributeOverride(name = "phone2.localNumber", column = @Column(name = "customer_phone2_local_number")) + @AttributeOverride(name = "phone2.ext", column = @Column(name = "customer_phone2_ext")) + @AttributeOverride(name = "phone2.dialOut", column = @Column(name = "customer_phone2_dial_out")) + @AttributeOverride(name = "phone2.internationalPrefix", column = @Column(name = "customer_phone2_international_prefix")) + @AttributeOverride(name = "phone2.ituPhone", column = @Column(name = "customer_phone2_itu_phone")) @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "customer_lan")) @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "customer_mac")) @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "customer_email1")) @@ -158,16 +173,6 @@ public class CustomerEntity extends IdentifiedObject { @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List statements = new ArrayList<>(); - /** - * Phone numbers for this customer's organisation. - * Managed via separate PhoneNumberEntity to avoid column conflicts. - * Initialized to empty ArrayList to avoid null pointer exceptions. - */ - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JoinColumn(name = "parent_entity_uuid", referencedColumnName = "id") - @SQLRestriction("parent_entity_type = 'CustomerEntity'") - private List phoneNumbers = new ArrayList<>(); - /** * Embeddable class for Status */ diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java index 8e9c9bfe..ae59ea37 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java @@ -25,6 +25,7 @@ import lombok.*; import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress; import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress; +import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber; import java.io.Serializable; @@ -59,8 +60,17 @@ public class Organisation implements Serializable { @Embedded private StreetAddress postalAddress; - // PhoneNumber fields removed - phone numbers are managed separately via PhoneNumberEntity - // to avoid JPA column mapping conflicts in embedded contexts + /** + * Primary phone number for this organisation. + */ + @Embedded + private TelephoneNumber phone1; + + /** + * Secondary phone number for this organisation. + */ + @Embedded + private TelephoneNumber phone2; /** * Electronic address for this organisation. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java deleted file mode 100644 index b232a7a3..00000000 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - * Copyright (c) 2025 Green Button Alliance, Inc. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.greenbuttonalliance.espi.common.domain.customer.entity; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; - -import jakarta.persistence.*; - -/** - * Embeddable class for OrganisationRole information. - * - * Identifies a way in which an organisation may participate in the utility enterprise. - * Organisation roles are not mutually exclusive; hence one organisation typically has many roles. - * This is an embeddable component, not a standalone entity. - */ -@Embeddable -@Data -@NoArgsConstructor -@ToString -public class OrganisationRole { - - /** - * Organisation having this role. - */ - @Embedded - @AttributeOverride(name = "organisationName", column = @Column(name = "role_organisation_name")) - @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "role_street_detail")) - @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "role_town_detail")) - @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "role_state_or_province")) - @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "role_postal_code")) - @AttributeOverride(name = "streetAddress.country", column = @Column(name = "role_country")) - @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "role_postal_street_detail")) - @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "role_postal_town_detail")) - @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "role_postal_state_or_province")) - @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "role_postal_postal_code")) - @AttributeOverride(name = "postalAddress.country", column = @Column(name = "role_postal_country")) - @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "role_email1")) - @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "role_email2")) - @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "role_web")) - @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "role_radio")) - private Organisation organisation; -} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java deleted file mode 100644 index a4ce5b6c..00000000 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * - * Copyright (c) 2025 Green Button Alliance, Inc. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.greenbuttonalliance.espi.common.domain.customer.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.proxy.HibernateProxy; -import org.hibernate.type.SqlTypes; - -import java.util.Objects; -import java.util.UUID; - -/** - * JPA entity for PhoneNumber to resolve embedded mapping conflicts. - * - * Separate entity table for phone numbers to eliminate column duplication - * issues when multiple entities embed Organisation with PhoneNumber fields. - * - * Note: PhoneNumber does NOT extend IdentifiedObject per ESPI 4.0 specification. - * It is not a top-level resource with selfLink/upLink/relatedLinks. - */ -@Entity -@Table(name = "phone_numbers") -@Getter -@Setter -@NoArgsConstructor -public class PhoneNumberEntity { - - /** - * Primary key identifier. - */ - @Id - @GeneratedValue(strategy = GenerationType.UUID) - @JdbcTypeCode(SqlTypes.CHAR) - @Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false) - private UUID id; - - /** - * Country code (per customer.xsd TelephoneNumber). - */ - @Column(name = "country_code", length = 256) - private String countryCode; - - /** - * Area or region code (per customer.xsd TelephoneNumber). - */ - @Column(name = "area_code", length = 256) - private String areaCode; - - /** - * City code (per customer.xsd TelephoneNumber). - */ - @Column(name = "city_code", length = 256) - private String cityCode; - - /** - * Main (local) part of this telephone number (per customer.xsd TelephoneNumber). - */ - @Column(name = "local_number", length = 256) - private String localNumber; - - /** - * Extension for this telephone number (per customer.xsd TelephoneNumber "ext" element). - */ - @Column(name = "extension", length = 256) - private String extension; - - /** - * Dial out code, for instance to call outside an enterprise (per customer.xsd TelephoneNumber). - */ - @Column(name = "dial_out", length = 256) - private String dialOut; - - /** - * Prefix used when calling an international number (per customer.xsd TelephoneNumber). - */ - @Column(name = "international_prefix", length = 256) - private String internationalPrefix; - - /** - * Phone number according to ITU E.164 (per customer.xsd TelephoneNumber). - */ - @Column(name = "itu_phone", length = 256) - private String ituPhone; - - /** - * Type of phone number (PRIMARY, SECONDARY, etc.). - */ - @Column(name = "phone_type", length = 20) - @Enumerated(EnumType.STRING) - private PhoneType phoneType; - - /** - * Reference to the parent entity UUID that owns this phone number. - * This is a generic reference that can point to any entity type. - */ - @Column(name = "parent_entity_uuid", length = 36) - private String parentEntityUuid; - - /** - * Type of the parent entity (CustomerEntity, ServiceSupplierEntity, etc.). - */ - @Column(name = "parent_entity_type", length = 100) - private String parentEntityType; - - /** - * Enum for phone number types. - */ - public enum PhoneType { - PRIMARY, - SECONDARY, - LOCATION_PRIMARY, - LOCATION_SECONDARY - } - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - PhoneNumberEntity that = (PhoneNumberEntity) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + - "id = " + getId() + ", " + - "areaCode = " + getAreaCode() + ", " + - "cityCode = " + getCityCode() + ", " + - "localNumber = " + getLocalNumber() + ", " + - "extension = " + getExtension() + ", " + - "phoneType = " + getPhoneType() + ", " + - "parentEntityUuid = " + getParentEntityUuid() + ", " + - "parentEntityType = " + getParentEntityType() + ")"; - } -} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java index a9b3886e..a8ef8ff4 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java @@ -24,11 +24,9 @@ import org.greenbuttonalliance.espi.common.domain.customer.enums.SupplierKind; import jakarta.persistence.*; -import org.hibernate.annotations.SQLRestriction; import org.hibernate.proxy.HibernateProxy; import java.time.OffsetDateTime; -import java.util.List; import java.util.Objects; /** @@ -59,6 +57,22 @@ public class ServiceSupplierEntity extends IdentifiedObject { @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "supplier_postal_state_or_province")), @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "supplier_postal_postal_code")), @AttributeOverride(name = "postalAddress.country", column = @Column(name = "supplier_postal_country")), + @AttributeOverride(name = "phone1.countryCode", column = @Column(name = "supplier_phone1_country_code")), + @AttributeOverride(name = "phone1.areaCode", column = @Column(name = "supplier_phone1_area_code")), + @AttributeOverride(name = "phone1.cityCode", column = @Column(name = "supplier_phone1_city_code")), + @AttributeOverride(name = "phone1.localNumber", column = @Column(name = "supplier_phone1_local_number")), + @AttributeOverride(name = "phone1.ext", column = @Column(name = "supplier_phone1_ext")), + @AttributeOverride(name = "phone1.dialOut", column = @Column(name = "supplier_phone1_dial_out")), + @AttributeOverride(name = "phone1.internationalPrefix", column = @Column(name = "supplier_phone1_international_prefix")), + @AttributeOverride(name = "phone1.ituPhone", column = @Column(name = "supplier_phone1_itu_phone")), + @AttributeOverride(name = "phone2.countryCode", column = @Column(name = "supplier_phone2_country_code")), + @AttributeOverride(name = "phone2.areaCode", column = @Column(name = "supplier_phone2_area_code")), + @AttributeOverride(name = "phone2.cityCode", column = @Column(name = "supplier_phone2_city_code")), + @AttributeOverride(name = "phone2.localNumber", column = @Column(name = "supplier_phone2_local_number")), + @AttributeOverride(name = "phone2.ext", column = @Column(name = "supplier_phone2_ext")), + @AttributeOverride(name = "phone2.dialOut", column = @Column(name = "supplier_phone2_dial_out")), + @AttributeOverride(name = "phone2.internationalPrefix", column = @Column(name = "supplier_phone2_international_prefix")), + @AttributeOverride(name = "phone2.ituPhone", column = @Column(name = "supplier_phone2_itu_phone")), @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "supplier_lan")), @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "supplier_mac")), @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "supplier_email1")), @@ -91,15 +105,6 @@ public class ServiceSupplierEntity extends IdentifiedObject { @Column(name = "effective_date") private OffsetDateTime effectiveDate; - /** - * Phone numbers for this service supplier's organisation. - * Managed via separate PhoneNumberEntity to avoid column conflicts. - */ - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JoinColumn(name = "parent_entity_uuid", referencedColumnName = "id") - @SQLRestriction("parent_entity_type = 'ServiceSupplierEntity'") - private List phoneNumbers; - @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/SupplierKind.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/SupplierKind.java index d5417117..9881f8d5 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/SupplierKind.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/SupplierKind.java @@ -20,38 +20,40 @@ package org.greenbuttonalliance.espi.common.domain.customer.enums; /** - * Enumeration for SupplierKind values. - * + * Enumeration for SupplierKind values per ESPI 4.0 customer.xsd. + * * Kind of supplier based on the energy market business rules. + * + * IMPORTANT: Sequence must match XSD exactly - ESPI uses ordinal values (0-5) for serialization. */ public enum SupplierKind { /** - * Utility supplier + * Utility supplier (ordinal 0) */ UTILITY, - + /** - * Retail energy supplier + * Retail energy supplier (ordinal 1) */ - RETAIL, - + RETAILER, + /** - * Generation supplier + * Other supplier type (ordinal 2) */ - GENERATION, - + OTHER, + /** - * Transmission supplier + * Load Serving Entity (ordinal 3) */ - TRANSMISSION, - + LSE, + /** - * Distribution supplier + * Meter Data Management Agent (ordinal 4) */ - DISTRIBUTION, - + MDMA, + /** - * Other supplier + * Metering Service Provider (ordinal 5) */ - OTHER + MSP } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/atom/CustomerAtomEntryDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/atom/CustomerAtomEntryDto.java index 2fe98a37..afa492c2 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/atom/CustomerAtomEntryDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/atom/CustomerAtomEntryDto.java @@ -55,9 +55,8 @@ public class CustomerAtomEntryDto extends AtomEntryDto { @XmlElement(name = "ProgramDateIdMappings", type = ProgramDateIdMappingsDto.class, namespace = "http://naesb.org/espi/customer"), @XmlElement(name = "ServiceLocation", type = ServiceLocationDto.class, namespace = "http://naesb.org/espi/customer"), @XmlElement(name = "Statement", type = StatementDto.class, namespace = "http://naesb.org/espi/customer"), - @XmlElement(name = "StatementRef", type = StatementRefDto.class, namespace = "http://naesb.org/espi/customer") - // ServiceSupplier - will be activated when ServiceSupplier DTO is added as part of Issue #28 - // @XmlElement(name = "ServiceSupplier", type = ServiceSupplierDto.class, namespace = "http://naesb.org/espi/customer") + @XmlElement(name = "StatementRef", type = StatementRefDto.class, namespace = "http://naesb.org/espi/customer"), + @XmlElement(name = "ServiceSupplier", type = ServiceSupplierDto.class, namespace = "http://naesb.org/espi/customer") }) protected Object content; diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java index 3a660ab6..f4302082 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java @@ -142,7 +142,7 @@ public class CustomerAccountDto { * responsible for billing and payment of CustomerAccount. */ @XmlElement(name = "contactInfo", namespace = "http://naesb.org/espi/customer") - private CustomerDto.OrganisationDto contactInfo; + private OrganisationDto contactInfo; /** * [extension] Customer account identifier. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java index 53be1f10..37af6d23 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java @@ -97,38 +97,6 @@ public static class PriorityDto { private String type; } - /** - * Embeddable DTO for Organisation. - * Field order matches customer.xsd:1096-1125. - */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "Organisation", namespace = "http://naesb.org/espi/customer", propOrder = { - "streetAddress", "postalAddress", "phone1", "phone2", "electronicAddress", "organisationName" - }) - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class OrganisationDto { - @XmlElement(name = "streetAddress", namespace = "http://naesb.org/espi/customer") - private StreetAddressDto streetAddress; - - @XmlElement(name = "postalAddress", namespace = "http://naesb.org/espi/customer") - private StreetAddressDto postalAddress; - - @XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") - private TelephoneNumberDto phone1; - - @XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") - private TelephoneNumberDto phone2; - - @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") - private ElectronicAddressDto electronicAddress; - - @XmlElement(name = "organisationName", namespace = "http://naesb.org/espi/customer") - private String organisationName; - } - /** * Embeddable DTO for StreetAddress. */ @@ -155,45 +123,4 @@ public static class StreetAddressDto { private String country; } - /** - * Embeddable DTO for PhoneNumber. - */ - /** - * TelephoneNumber DTO nested class. - * Per customer.xsd TelephoneNumber type (lines 1428-1478). - * 8 fields per ESPI 4.0 specification. - */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "TelephoneNumber", namespace = "http://naesb.org/espi/customer", propOrder = { - "countryCode", "areaCode", "cityCode", "localNumber", "ext", "dialOut", "internationalPrefix", "ituPhone" - }) - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class TelephoneNumberDto implements Serializable { - @XmlElement(name = "countryCode", namespace = "http://naesb.org/espi/customer") - private String countryCode; - - @XmlElement(name = "areaCode", namespace = "http://naesb.org/espi/customer") - private String areaCode; - - @XmlElement(name = "cityCode", namespace = "http://naesb.org/espi/customer") - private String cityCode; - - @XmlElement(name = "localNumber", namespace = "http://naesb.org/espi/customer") - private String localNumber; - - @XmlElement(name = "ext", namespace = "http://naesb.org/espi/customer") - private String ext; - - @XmlElement(name = "dialOut", namespace = "http://naesb.org/espi/customer") - private String dialOut; - - @XmlElement(name = "internationalPrefix", namespace = "http://naesb.org/espi/customer") - private String internationalPrefix; - - @XmlElement(name = "ituPhone", namespace = "http://naesb.org/espi/customer") - private String ituPhone; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDto.java new file mode 100644 index 00000000..b74fad44 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDto.java @@ -0,0 +1,92 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import jakarta.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +/** + * Organisation DTO class for JAXB XML marshalling/unmarshalling. + * + * Organisation that might have roles as utility, customer, supplier, manufacturer, etc. + * Field order matches customer.xsd:1096-1125. + */ +@XmlRootElement(name = "Organisation", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Organisation", namespace = "http://naesb.org/espi/customer", propOrder = { + "streetAddress", "postalAddress", "phone1", "phone2", "electronicAddress", "organisationName" +}) +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class OrganisationDto implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Street address for this organisation. + * Maps to customer.xsd streetAddress element (lines 1097-1105). + */ + @XmlElement(name = "streetAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.StreetAddressDto streetAddress; + + /** + * Postal address for this organisation. + * Maps to customer.xsd postalAddress element (lines 1106-1114). + */ + @XmlElement(name = "postalAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.StreetAddressDto postalAddress; + + /** + * Primary phone number for this organisation. + * Maps to customer.xsd phone1 element (lines 1115-1116). + */ + @XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") + private TelephoneNumberDto phone1; + + /** + * Secondary phone number for this organisation. + * Maps to customer.xsd phone2 element (lines 1117-1118). + */ + @XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") + private TelephoneNumberDto phone2; + + /** + * Electronic address for this organisation. + * Maps to customer.xsd electronicAddress element (lines 1119-1120). + */ + @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") + private ElectronicAddressDto electronicAddress; + + /** + * Organisation name. + * Maps to customer.xsd organisationName element (lines 1121-1124). + */ + @XmlElement(name = "organisationName", namespace = "http://naesb.org/espi/customer") + private String organisationName; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java index 65006060..c381ebb8 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java @@ -74,13 +74,13 @@ public class ServiceLocationDto implements Serializable { * Primary phone number for this service location. */ @XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") - private CustomerDto.TelephoneNumberDto phone1; + private TelephoneNumberDto phone1; /** * Secondary phone number for this service location. */ @XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") - private CustomerDto.TelephoneNumberDto phone2; + private TelephoneNumberDto phone2; /** * Electronic address (email, web, etc.). diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDto.java new file mode 100644 index 00000000..d923d96a --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDto.java @@ -0,0 +1,81 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.enums.SupplierKind; + +import jakarta.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +/** + * ServiceSupplier DTO class for JAXB XML marshalling/unmarshalling. + * + * Represents an organization that provides services to customers. + * Field order matches customer.xsd:1159-1186. + */ +@XmlRootElement(name = "ServiceSupplier", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ServiceSupplier", namespace = "http://naesb.org/espi/customer", propOrder = { + "organisation", "kind", "issuerIdentificationNumber", "effectiveDate" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ServiceSupplierDto implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Organisation having this role. + * Maps to customer.xsd Organisation element (lines 1162-1163). + */ + @XmlElement(name = "Organisation", namespace = "http://naesb.org/espi/customer") + private OrganisationDto organisation; + + /** + * Kind of supplier. + * Maps to customer.xsd SupplierKind element (lines 1164-1174). + */ + @XmlElement(name = "kind", namespace = "http://naesb.org/espi/customer") + private SupplierKind kind; + + /** + * Unique transaction reference prefix number issued to an entity by the International Organization for + * Standardization for the purpose of tagging onto electronic financial transactions, as defined in + * ISO/IEC 7812-1 and ISO/IEC 7812-2. + * Maps to customer.xsd issuerIdentificationNumber element (lines 1175-1181). + */ + @XmlElement(name = "issuerIdentificationNumber", namespace = "http://naesb.org/espi/customer") + private String issuerIdentificationNumber; + + /** + * [extension] Effective Date of Service Activation. + * Maps to customer.xsd effectiveDate element (lines 1182-1185). + */ + @XmlElement(name = "effectiveDate", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime effectiveDate; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDto.java new file mode 100644 index 00000000..f5469622 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDto.java @@ -0,0 +1,106 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import jakarta.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +/** + * TelephoneNumber DTO class for JAXB XML marshalling/unmarshalling. + * + * Telephone number. + * Field order matches customer.xsd:1428-1478. + */ +@XmlRootElement(name = "TelephoneNumber", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "TelephoneNumber", namespace = "http://naesb.org/espi/customer", propOrder = { + "countryCode", "areaCode", "cityCode", "localNumber", "ext", "dialOut", "internationalPrefix", "ituPhone" +}) +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class TelephoneNumberDto implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Country code. + * Maps to customer.xsd countryCode element (lines 1431-1436). + */ + @XmlElement(name = "countryCode", namespace = "http://naesb.org/espi/customer") + private String countryCode; + + /** + * Area or city code. + * Maps to customer.xsd areaCode element (lines 1437-1442). + */ + @XmlElement(name = "areaCode", namespace = "http://naesb.org/espi/customer") + private String areaCode; + + /** + * City code. + * Maps to customer.xsd cityCode element (lines 1443-1448). + */ + @XmlElement(name = "cityCode", namespace = "http://naesb.org/espi/customer") + private String cityCode; + + /** + * Local number. + * Maps to customer.xsd localNumber element (lines 1449-1454). + */ + @XmlElement(name = "localNumber", namespace = "http://naesb.org/espi/customer") + private String localNumber; + + /** + * Extension. + * Maps to customer.xsd ext element (lines 1455-1460). + */ + @XmlElement(name = "ext", namespace = "http://naesb.org/espi/customer") + private String ext; + + /** + * Dial out code, e.g., '0'. + * Maps to customer.xsd dialOut element (lines 1461-1466). + */ + @XmlElement(name = "dialOut", namespace = "http://naesb.org/espi/customer") + private String dialOut; + + /** + * International prefix, e.g., '00', '+'. + * Maps to customer.xsd internationalPrefix element (lines 1467-1472). + */ + @XmlElement(name = "internationalPrefix", namespace = "http://naesb.org/espi/customer") + private String internationalPrefix; + + /** + * ITU-T phone number per E.164. + * Maps to customer.xsd ituPhone element (lines 1473-1477). + */ + @XmlElement(name = "ituPhone", namespace = "http://naesb.org/espi/customer") + private String ituPhone; +} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java index 52a063b3..af24e04d 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java @@ -167,14 +167,6 @@ public class ElectricPowerQualitySummaryDto { @XmlElement(name = "tempOvervoltage") private Long tempOvervoltage; - /** - * Reference to the usage point this power quality summary belongs to. - * Represents the logical relationship to the measurement point. - */ - @XmlTransient - private Long usagePointId; - - /** * Checks if this summary contains voltage quality measurements. * diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java index ff1a2e57..c856811f 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java @@ -20,21 +20,11 @@ package org.greenbuttonalliance.espi.common.mapper.customer; import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity; -import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress; -import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; -import org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity; -import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress; import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; -import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; -import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper; import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils; import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.MappingTarget; -import org.mapstruct.Named; - -import java.util.List; /** * MapStruct mapper for converting between CustomerEntity and CustomerDto. @@ -47,9 +37,9 @@ */ @Mapper(componentModel = "spring", uses = { DateTimeMapper.class, - ElectronicAddressMapper.class + OrganisationMapper.class }) -public interface CustomerMapper extends BaseMapperUtils { +public interface CustomerMapper { /** * Converts a CustomerEntity to a CustomerDto. @@ -58,7 +48,7 @@ public interface CustomerMapper extends BaseMapperUtils { * @param entity the customer entity * @return the customer DTO */ - @Mapping(target = "organisation", source = ".", qualifiedByName = "mapOrganisation") + @Mapping(target = "organisation", source = "organisation") @Mapping(target = "kind", source = "kind") @Mapping(target = "specialNeed", source = "specialNeed") @Mapping(target = "vip", source = "vip") @@ -76,8 +66,7 @@ public interface CustomerMapper extends BaseMapperUtils { * @param dto the customer DTO * @return the customer entity */ - @Mapping(target = "organisation", source = "organisation", qualifiedByName = "mapOrganisationFromDto") - @Mapping(target = "phoneNumbers", ignore = true) + @Mapping(target = "organisation", source = "organisation") @Mapping(target = "kind", source = "kind") @Mapping(target = "specialNeed", source = "specialNeed") @Mapping(target = "vip", source = "vip") @@ -90,135 +79,4 @@ public interface CustomerMapper extends BaseMapperUtils { @Mapping(target = "timeConfiguration", ignore = true) @Mapping(target = "statements", ignore = true) CustomerEntity toEntity(CustomerDto dto); - - /** - * Maps CustomerEntity with PhoneNumberEntity list to OrganisationDto. - * Combines embedded Organisation data with separate phone number entities. - * Field order matches customer.xsd:1096-1125. - */ - @Named("mapOrganisation") - default CustomerDto.OrganisationDto mapOrganisation(CustomerEntity entity) { - if (entity == null || entity.getOrganisation() == null) { - return null; - } - - Organisation org = entity.getOrganisation(); - List phoneNumbers = entity.getPhoneNumbers(); - - // Extract phone numbers by type - CustomerDto.TelephoneNumberDto phone1 = extractPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.PRIMARY); - CustomerDto.TelephoneNumberDto phone2 = extractPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.SECONDARY); - - // Constructor order: streetAddress, postalAddress, phone1, phone2, electronicAddress, organisationName - return new CustomerDto.OrganisationDto( - mapStreetAddress(org.getStreetAddress()), - mapStreetAddress(org.getPostalAddress()), - phone1, - phone2, - mapElectronicAddress(org.getElectronicAddress()), - org.getOrganisationName() - ); - } - - /** - * Maps OrganisationDto to Organisation entity (without phone numbers). - * Phone numbers are handled separately via PhoneNumberEntity. - */ - @Named("mapOrganisationFromDto") - default Organisation mapOrganisationFromDto(CustomerDto.OrganisationDto orgDto) { - if (orgDto == null) { - return null; - } - - Organisation org = new Organisation(); - org.setOrganisationName(orgDto.getOrganisationName()); - org.setStreetAddress(mapStreetAddressFromDto(orgDto.getStreetAddress())); - org.setPostalAddress(mapStreetAddressFromDto(orgDto.getPostalAddress())); - org.setElectronicAddress(mapElectronicAddressFromDto(orgDto.getElectronicAddress())); - - // Phone numbers are @Transient in Organisation and managed separately - return org; - } - - // Helper methods for address mapping - default CustomerDto.StreetAddressDto mapStreetAddress(StreetAddress address) { - if (address == null) return null; - return new CustomerDto.StreetAddressDto( - address.getStreetDetail(), - address.getTownDetail(), - address.getStateOrProvince(), - address.getPostalCode(), - address.getCountry() - ); - } - - default StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAddressDto dto) { - if (dto == null) return null; - StreetAddress address = new StreetAddress(); - address.setStreetDetail(dto.getStreetDetail()); - address.setTownDetail(dto.getTownDetail()); - address.setStateOrProvince(dto.getStateOrProvince()); - address.setPostalCode(dto.getPostalCode()); - address.setCountry(dto.getCountry()); - return address; - } - - /** - * Helper method for mapping electronic address in custom Organisation mapping. - * Delegates to simple field-to-field copy since ElectronicAddressMapper - * is not directly accessible from default interface methods. - */ - default ElectronicAddressDto mapElectronicAddress(ElectronicAddress address) { - if (address == null) return null; - return new ElectronicAddressDto( - address.getLan(), - address.getMac(), - address.getEmail1(), - address.getEmail2(), - address.getWeb(), - address.getRadio(), - address.getUserID(), - address.getPassword() - ); - } - - /** - * Helper method for mapping electronic address from DTO in custom Organisation mapping. - * Delegates to simple field-to-field copy since ElectronicAddressMapper - * is not directly accessible from default interface methods. - */ - default ElectronicAddress mapElectronicAddressFromDto(ElectronicAddressDto dto) { - if (dto == null) return null; - ElectronicAddress address = new ElectronicAddress(); - address.setLan(dto.getLan()); - address.setMac(dto.getMac()); - address.setEmail1(dto.getEmail1()); - address.setEmail2(dto.getEmail2()); - address.setWeb(dto.getWeb()); - address.setRadio(dto.getRadio()); - address.setUserID(dto.getUserID()); - address.setPassword(dto.getPassword()); - return address; - } - - // Helper method to extract phone number by type - // Maps PhoneNumberEntity (old 4-field format) to TelephoneNumberDto (new 8-field format) - default CustomerDto.TelephoneNumberDto extractPhoneByType(List phoneNumbers, PhoneNumberEntity.PhoneType type) { - if (phoneNumbers == null) return null; - - return phoneNumbers.stream() - .filter(phone -> phone.getPhoneType() == type) - .findFirst() - .map(phone -> new CustomerDto.TelephoneNumberDto( - phone.getCountryCode(), // 1. countryCode - phone.getAreaCode(), // 2. areaCode - phone.getCityCode(), // 3. cityCode - phone.getLocalNumber(), // 4. localNumber - phone.getExtension(), // 5. ext (mapped from extension) - phone.getDialOut(), // 6. dialOut - phone.getInternationalPrefix(), // 7. internationalPrefix - phone.getItuPhone() // 8. ituPhone - )) - .orElse(null); - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/OrganisationMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/OrganisationMapper.java index 05deaa72..dcc72087 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/OrganisationMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/OrganisationMapper.java @@ -20,36 +20,49 @@ package org.greenbuttonalliance.espi.common.mapper.customer; import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; -import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; +import org.greenbuttonalliance.espi.common.dto.customer.OrganisationDto; import org.mapstruct.Mapper; import org.mapstruct.Mapping; /** * MapStruct mapper for converting between Organisation entity and DTO. + * + * Handles embedded StreetAddress, TelephoneNumber, and ElectronicAddress mappings. */ @Mapper(componentModel = "spring", uses = { StreetAddressMapper.class, + TelephoneNumberMapper.class, ElectronicAddressMapper.class }) public interface OrganisationMapper { /** * Converts an Organisation entity to a DTO. - * Note: phone1 and phone2 are not included in the entity due to JPA column mapping conflicts. + * Maps all embedded objects (addresses, phones, electronic address). * * @param entity the organisation entity * @return the organisation DTO */ - @Mapping(target = "phone1", ignore = true) // Not in entity - managed separately - @Mapping(target = "phone2", ignore = true) // Not in entity - managed separately - CustomerDto.OrganisationDto toDto(Organisation entity); + @Mapping(target = "streetAddress", source = "streetAddress") + @Mapping(target = "postalAddress", source = "postalAddress") + @Mapping(target = "phone1", source = "phone1") + @Mapping(target = "phone2", source = "phone2") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "organisationName", source = "organisationName") + OrganisationDto toDto(Organisation entity); /** * Converts an Organisation DTO to an entity. - * Note: phone1 and phone2 are not included in the entity due to JPA column mapping conflicts. + * Maps all embedded objects from DTO to entity. * * @param dto the organisation DTO * @return the organisation entity */ - Organisation toEntity(CustomerDto.OrganisationDto dto); + @Mapping(target = "streetAddress", source = "streetAddress") + @Mapping(target = "postalAddress", source = "postalAddress") + @Mapping(target = "phone1", source = "phone1") + @Mapping(target = "phone2", source = "phone2") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "organisationName", source = "organisationName") + Organisation toEntity(OrganisationDto dto); } diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java index 50c9df6e..17959b43 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java @@ -45,7 +45,8 @@ */ @Mapper(componentModel = "spring", uses = { DateTimeMapper.class, - ElectronicAddressMapper.class + ElectronicAddressMapper.class, + TelephoneNumberMapper.class }) public interface ServiceLocationMapper { @@ -132,39 +133,6 @@ default StreetAddress map(CustomerDto.StreetAddressDto dto) { return address; } - /** - * Maps TelephoneNumber entity to CustomerDto.TelephoneNumberDto. - */ - default CustomerDto.TelephoneNumberDto mapTelephone(TelephoneNumber phone) { - if (phone == null) return null; - return new CustomerDto.TelephoneNumberDto( - phone.getCountryCode(), - phone.getAreaCode(), - phone.getCityCode(), - phone.getLocalNumber(), - phone.getExt(), - phone.getDialOut(), - phone.getInternationalPrefix(), - phone.getItuPhone() - ); - } - - /** - * Maps CustomerDto.TelephoneNumberDto to TelephoneNumber entity. - */ - default TelephoneNumber mapTelephone(CustomerDto.TelephoneNumberDto dto) { - if (dto == null) return null; - return new TelephoneNumber( - dto.getCountryCode(), - dto.getAreaCode(), - dto.getCityCode(), - dto.getLocalNumber(), - dto.getExt(), - dto.getDialOut(), - dto.getInternationalPrefix(), - dto.getItuPhone() - ); - } /** * Maps Status entity to StatusDto. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceSupplierMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceSupplierMapper.java new file mode 100644 index 00000000..27bc48b7 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceSupplierMapper.java @@ -0,0 +1,97 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceSupplierEntity; +import org.greenbuttonalliance.espi.common.dto.customer.ServiceSupplierDto; +import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +/** + * MapStruct mapper for converting between ServiceSupplierEntity and ServiceSupplierDto. + * + * Maps only ServiceSupplier fields. IdentifiedObject fields are NOT part of the customer.xsd + * definition and are handled by AtomFeedDto/AtomEntryDto. + * + * Handles the conversion between the JPA entity used for persistence and the DTO + * used for JAXB XML marshalling in the Green Button API. + */ +@Mapper(componentModel = "spring", uses = { + DateTimeMapper.class, + OrganisationMapper.class +}) +public interface ServiceSupplierMapper { + + /** + * Converts a ServiceSupplierEntity to a ServiceSupplierDto. + * Maps service supplier information including embedded organisation. + * + * @param entity the service supplier entity + * @return the service supplier DTO + */ + @Mapping(target = "organisation", source = "organisation") + @Mapping(target = "kind", source = "kind") + @Mapping(target = "issuerIdentificationNumber", source = "issuerIdentificationNumber") + @Mapping(target = "effectiveDate", source = "effectiveDate") + ServiceSupplierDto toDto(ServiceSupplierEntity entity); + + /** + * Converts a ServiceSupplierDto to a ServiceSupplierEntity. + * Maps service supplier information from DTO to entity. + * + * @param dto the service supplier DTO + * @return the service supplier entity + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "selfLink", ignore = true) + @Mapping(target = "upLink", ignore = true) + @Mapping(target = "published", ignore = true) + @Mapping(target = "updated", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "description", ignore = true) + @Mapping(target = "organisation", source = "organisation") + @Mapping(target = "kind", source = "kind") + @Mapping(target = "issuerIdentificationNumber", source = "issuerIdentificationNumber") + @Mapping(target = "effectiveDate", source = "effectiveDate") + ServiceSupplierEntity toEntity(ServiceSupplierDto dto); + + /** + * Updates an existing ServiceSupplierEntity with data from a ServiceSupplierDto. + * Does not update IdentifiedObject fields (id, selfLink, upLink, published, updated, created, description). + * + * @param dto the service supplier DTO with updated data + * @param entity the existing service supplier entity to update + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "selfLink", ignore = true) + @Mapping(target = "upLink", ignore = true) + @Mapping(target = "published", ignore = true) + @Mapping(target = "updated", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "description", ignore = true) + @Mapping(target = "organisation", source = "organisation") + @Mapping(target = "kind", source = "kind") + @Mapping(target = "issuerIdentificationNumber", source = "issuerIdentificationNumber") + @Mapping(target = "effectiveDate", source = "effectiveDate") + void updateEntityFromDto(ServiceSupplierDto dto, @MappingTarget ServiceSupplierEntity entity); +} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/TelephoneNumberMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/TelephoneNumberMapper.java new file mode 100644 index 00000000..9824155e --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/TelephoneNumberMapper.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber; +import org.greenbuttonalliance.espi.common.dto.customer.TelephoneNumberDto; +import org.mapstruct.Mapper; + +/** + * MapStruct mapper for converting between TelephoneNumber entity and DTO. + */ +@Mapper(componentModel = "spring") +public interface TelephoneNumberMapper { + + /** + * Converts a TelephoneNumber entity to a DTO. + * + * @param entity the telephone number entity + * @return the telephone number DTO + */ + TelephoneNumberDto toDto(TelephoneNumber entity); + + /** + * Converts a TelephoneNumber DTO to an entity. + * + * @param dto the telephone number DTO + * @return the telephone number entity + */ + TelephoneNumber toEntity(TelephoneNumberDto dto); +} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java index 73f6452b..6c36049d 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java @@ -39,7 +39,6 @@ */ @Mapper(componentModel = "spring", uses = { DateTimeMapper.class, - BaseMapperUtils.class, DateTimeIntervalMapper.class }) public interface ElectricPowerQualitySummaryMapper { @@ -51,7 +50,6 @@ public interface ElectricPowerQualitySummaryMapper { * @param entity the electric power quality summary entity * @return the electric power quality summary DTO */ - @Mapping(target = "usagePointId", source = "usagePoint.id", qualifiedByName = "uuidToLong") ElectricPowerQualitySummaryDto toDto(ElectricPowerQualitySummaryEntity entity); /** diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/PhoneNumberService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/PhoneNumberService.java deleted file mode 100644 index 7da213db..00000000 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/PhoneNumberService.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * - * Copyright (c) 2025 Green Button Alliance, Inc. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.greenbuttonalliance.espi.common.service; - -import org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Service for managing PhoneNumberEntity relationships across different parent entities. - * - * Provides utilities for creating, updating, and managing phone numbers as separate entities - * while maintaining the logical association with parent entities (Customer, ServiceSupplier, etc.). - */ -@Service -public class PhoneNumberService { - - /** - * Creates phone number entities for a given parent entity. - * - * @param parentEntityUuid UUID of the parent entity - * @param parentEntityType Type of the parent entity (e.g., "CustomerEntity", "ServiceSupplierEntity") - * @param phone1AreaCode Primary phone area code - * @param phone1CityCode Primary phone city code - * @param phone1LocalNumber Primary phone local number - * @param phone1Extension Primary phone extension - * @param phone2AreaCode Secondary phone area code (optional) - * @param phone2CityCode Secondary phone city code (optional) - * @param phone2LocalNumber Secondary phone local number (optional) - * @param phone2Extension Secondary phone extension (optional) - * @return List of PhoneNumberEntity objects - */ - public List createPhoneNumbers( - String parentEntityUuid, - String parentEntityType, - String phone1AreaCode, - String phone1CityCode, - String phone1LocalNumber, - String phone1Extension, - String phone2AreaCode, - String phone2CityCode, - String phone2LocalNumber, - String phone2Extension) { - - List phoneNumbers = new ArrayList<>(); - - // Create primary phone if any field is provided - if (hasPhoneData(phone1AreaCode, phone1CityCode, phone1LocalNumber, phone1Extension)) { - PhoneNumberEntity primaryPhone = new PhoneNumberEntity(); - primaryPhone.setId(UUID.randomUUID()); - primaryPhone.setParentEntityUuid(parentEntityUuid); - primaryPhone.setParentEntityType(parentEntityType); - primaryPhone.setPhoneType(PhoneNumberEntity.PhoneType.PRIMARY); - primaryPhone.setAreaCode(phone1AreaCode); - primaryPhone.setCityCode(phone1CityCode); - primaryPhone.setLocalNumber(phone1LocalNumber); - primaryPhone.setExtension(phone1Extension); - phoneNumbers.add(primaryPhone); - } - - // Create secondary phone if any field is provided - if (hasPhoneData(phone2AreaCode, phone2CityCode, phone2LocalNumber, phone2Extension)) { - PhoneNumberEntity secondaryPhone = new PhoneNumberEntity(); - secondaryPhone.setId(UUID.randomUUID()); - secondaryPhone.setParentEntityUuid(parentEntityUuid); - secondaryPhone.setParentEntityType(parentEntityType); - secondaryPhone.setPhoneType(PhoneNumberEntity.PhoneType.SECONDARY); - secondaryPhone.setAreaCode(phone2AreaCode); - secondaryPhone.setCityCode(phone2CityCode); - secondaryPhone.setLocalNumber(phone2LocalNumber); - secondaryPhone.setExtension(phone2Extension); - phoneNumbers.add(secondaryPhone); - } - - return phoneNumbers; - } - - /** - * Updates existing phone number entities for a parent entity. - * Removes existing phone numbers and creates new ones based on provided data. - * - * @param existingPhoneNumbers Current phone number entities - * @param parentEntityUuid UUID of the parent entity - * @param parentEntityType Type of the parent entity - * @param phone1AreaCode Primary phone area code - * @param phone1CityCode Primary phone city code - * @param phone1LocalNumber Primary phone local number - * @param phone1Extension Primary phone extension - * @param phone2AreaCode Secondary phone area code (optional) - * @param phone2CityCode Secondary phone city code (optional) - * @param phone2LocalNumber Secondary phone local number (optional) - * @param phone2Extension Secondary phone extension (optional) - * @return Updated list of PhoneNumberEntity objects - */ - public List updatePhoneNumbers( - List existingPhoneNumbers, - String parentEntityUuid, - String parentEntityType, - String phone1AreaCode, - String phone1CityCode, - String phone1LocalNumber, - String phone1Extension, - String phone2AreaCode, - String phone2CityCode, - String phone2LocalNumber, - String phone2Extension) { - - // Clear existing phone numbers - if (existingPhoneNumbers != null) { - existingPhoneNumbers.clear(); - } - - // Create new phone numbers - return createPhoneNumbers( - parentEntityUuid, parentEntityType, - phone1AreaCode, phone1CityCode, phone1LocalNumber, phone1Extension, - phone2AreaCode, phone2CityCode, phone2LocalNumber, phone2Extension - ); - } - - /** - * Extracts a phone number by type from a list of phone number entities. - * - * @param phoneNumbers List of phone number entities - * @param phoneType Type of phone to extract - * @return PhoneNumberEntity of the specified type, or null if not found - */ - public PhoneNumberEntity getPhoneByType(List phoneNumbers, PhoneNumberEntity.PhoneType phoneType) { - if (phoneNumbers == null) { - return null; - } - - return phoneNumbers.stream() - .filter(phone -> phone.getPhoneType() == phoneType) - .findFirst() - .orElse(null); - } - - /** - * Gets the primary phone number from a list of phone number entities. - * - * @param phoneNumbers List of phone number entities - * @return Primary PhoneNumberEntity, or null if not found - */ - public PhoneNumberEntity getPrimaryPhone(List phoneNumbers) { - return getPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.PRIMARY); - } - - /** - * Gets the secondary phone number from a list of phone number entities. - * - * @param phoneNumbers List of phone number entities - * @return Secondary PhoneNumberEntity, or null if not found - */ - public PhoneNumberEntity getSecondaryPhone(List phoneNumbers) { - return getPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.SECONDARY); - } - - /** - * Checks if any phone data is provided. - * - * @param areaCode Area code - * @param cityCode City code - * @param localNumber Local number - * @param extension Extension - * @return true if any field has data, false otherwise - */ - private boolean hasPhoneData(String areaCode, String cityCode, String localNumber, String extension) { - return (areaCode != null && !areaCode.trim().isEmpty()) || - (cityCode != null && !cityCode.trim().isEmpty()) || - (localNumber != null && !localNumber.trim().isEmpty()) || - (extension != null && !extension.trim().isEmpty()); - } -} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java index f4d6feb5..d7838192 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java @@ -265,6 +265,10 @@ private Marshaller createMarshaller(Class dtoClass, Set requiredNames org.greenbuttonalliance.espi.common.dto.customer.StatementDto.class, org.greenbuttonalliance.espi.common.dto.customer.StatementRefDto.class, org.greenbuttonalliance.espi.common.dto.customer.StatusDto.class, + org.greenbuttonalliance.espi.common.dto.customer.ServiceSupplierDto.class, + org.greenbuttonalliance.espi.common.dto.customer.OrganisationDto.class, + org.greenbuttonalliance.espi.common.dto.customer.TelephoneNumberDto.class, + org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto.class, // Dynamic class parameter dtoClass 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 e334859c..29ed995a 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 @@ -141,6 +141,24 @@ CREATE TABLE customers customer_radio VARCHAR(255), customer_user_id VARCHAR(255), customer_password VARCHAR(255), + -- Organisation.phone1 (customer.xsd lines 942-989) + customer_phone1_country_code VARCHAR(10), + customer_phone1_area_code VARCHAR(10), + customer_phone1_city_code VARCHAR(10), + customer_phone1_local_number VARCHAR(30), + customer_phone1_ext VARCHAR(20), + customer_phone1_dial_out VARCHAR(10), + customer_phone1_international_prefix VARCHAR(10), + customer_phone1_itu_phone VARCHAR(50), + -- Organisation.phone2 (customer.xsd lines 990-1037) + customer_phone2_country_code VARCHAR(10), + customer_phone2_area_code VARCHAR(10), + customer_phone2_city_code VARCHAR(10), + customer_phone2_local_number VARCHAR(30), + customer_phone2_ext VARCHAR(20), + customer_phone2_dial_out VARCHAR(10), + customer_phone2_international_prefix VARCHAR(10), + customer_phone2_itu_phone VARCHAR(50), -- Status embedded object columns status_value VARCHAR(256), @@ -329,6 +347,24 @@ CREATE TABLE customer_accounts postal_state_or_province VARCHAR(256), postal_postal_code VARCHAR(256), postal_country VARCHAR(256), + -- contactInfo.phone1 (customer.xsd TelephoneNumber lines 1428-1478) + contact_phone1_country_code VARCHAR(10), + contact_phone1_area_code VARCHAR(10), + contact_phone1_city_code VARCHAR(10), + contact_phone1_local_number VARCHAR(30), + contact_phone1_ext VARCHAR(20), + contact_phone1_dial_out VARCHAR(10), + contact_phone1_international_prefix VARCHAR(10), + contact_phone1_itu_phone VARCHAR(50), + -- contactInfo.phone2 (customer.xsd TelephoneNumber lines 1428-1478) + contact_phone2_country_code VARCHAR(10), + contact_phone2_area_code VARCHAR(10), + contact_phone2_city_code VARCHAR(10), + contact_phone2_local_number VARCHAR(30), + contact_phone2_ext VARCHAR(20), + contact_phone2_dial_out VARCHAR(10), + contact_phone2_international_prefix VARCHAR(10), + contact_phone2_itu_phone VARCHAR(50), -- contactInfo.electronicAddress (customer.xsd lines 886-936) contact_lan VARCHAR(256), contact_mac VARCHAR(256), @@ -665,24 +701,24 @@ CREATE TABLE service_locations status_reason VARCHAR(256), -- Phone1 embedded object columns (customer.xsd TelephoneNumber type - 8 fields) - phone1_country_code VARCHAR(256), - phone1_area_code VARCHAR(256), - phone1_city_code VARCHAR(256), - phone1_local_number VARCHAR(256), - phone1_ext VARCHAR(256), - phone1_dial_out VARCHAR(256), - phone1_international_prefix VARCHAR(256), - phone1_itu_phone VARCHAR(256), + phone1_country_code VARCHAR(10), + phone1_area_code VARCHAR(10), + phone1_city_code VARCHAR(10), + phone1_local_number VARCHAR(30), + phone1_ext VARCHAR(20), + phone1_dial_out VARCHAR(10), + phone1_international_prefix VARCHAR(10), + phone1_itu_phone VARCHAR(50), -- Phone2 embedded object columns (customer.xsd TelephoneNumber type - 8 fields) - phone2_country_code VARCHAR(256), - phone2_area_code VARCHAR(256), - phone2_city_code VARCHAR(256), - phone2_local_number VARCHAR(256), - phone2_ext VARCHAR(256), - phone2_dial_out VARCHAR(256), - phone2_international_prefix VARCHAR(256), - phone2_itu_phone VARCHAR(256), + phone2_country_code VARCHAR(10), + phone2_area_code VARCHAR(10), + phone2_city_code VARCHAR(10), + phone2_local_number VARCHAR(30), + phone2_ext VARCHAR(20), + phone2_dial_out VARCHAR(10), + phone2_international_prefix VARCHAR(10), + phone2_itu_phone VARCHAR(50), -- Service location specific fields access_method VARCHAR(256), @@ -763,7 +799,23 @@ CREATE TABLE service_suppliers supplier_web VARCHAR(255), supplier_radio VARCHAR(255), supplier_user_id VARCHAR(256), - supplier_password VARCHAR(256) + supplier_password VARCHAR(256), + supplier_phone1_country_code VARCHAR(10), + supplier_phone1_area_code VARCHAR(10), + supplier_phone1_city_code VARCHAR(10), + supplier_phone1_local_number VARCHAR(30), + supplier_phone1_ext VARCHAR(20), + supplier_phone1_dial_out VARCHAR(10), + supplier_phone1_international_prefix VARCHAR(10), + supplier_phone1_itu_phone VARCHAR(50), + supplier_phone2_country_code VARCHAR(10), + supplier_phone2_area_code VARCHAR(10), + supplier_phone2_city_code VARCHAR(10), + supplier_phone2_local_number VARCHAR(30), + supplier_phone2_ext VARCHAR(20), + supplier_phone2_dial_out VARCHAR(10), + supplier_phone2_international_prefix VARCHAR(10), + supplier_phone2_itu_phone VARCHAR(50) ); CREATE INDEX idx_service_supplier_kind ON service_suppliers (kind); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java index 5297cf2a..f5de072c 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java @@ -217,7 +217,7 @@ private CustomerAccountDto createFullCustomerAccountDto() { "Account in good standing" ); - CustomerDto.OrganisationDto contactInfo = new CustomerDto.OrganisationDto( + OrganisationDto contactInfo = new OrganisationDto( new CustomerDto.StreetAddressDto("123 Main St", "Springfield", "IL", "62701", "USA"), null, null, null, new ElectronicAddressDto(null, null, "contact@acme.com", null, "https://acme.com", null, null, null), diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java index 7de9fc0a..b7bf86f9 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java @@ -69,7 +69,7 @@ void shouldMarshalCustomerWithAllFields() { "high-priority" // type ); - CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone1 = new TelephoneNumberDto( "1", // countryCode "415", // areaCode "555", // cityCode @@ -80,7 +80,7 @@ void shouldMarshalCustomerWithAllFields() { null // ituPhone ); - CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone2 = new TelephoneNumberDto( "1", "415", "555", @@ -118,7 +118,7 @@ void shouldMarshalCustomerWithAllFields() { "USA" ); - CustomerDto.OrganisationDto organisation = new CustomerDto.OrganisationDto( + OrganisationDto organisation = new OrganisationDto( streetAddress, postalAddress, phone1, diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java index 2f661b02..4c77f5e7 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java @@ -201,7 +201,7 @@ void shouldExportCustomerWithMinimalData() throws IOException { LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); - CustomerDto.OrganisationDto organisation = new CustomerDto.OrganisationDto( + OrganisationDto organisation = new OrganisationDto( null, null, null, null, null, "Minimal Org" ); @@ -273,11 +273,11 @@ private CustomerDto createFullCustomerDto() { "PO Box 456", "Springfield", "IL", "62702", "USA" ); - CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone1 = new TelephoneNumberDto( "1", "217", null, "555-1234", null, null, null, null ); - CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone2 = new TelephoneNumberDto( "1", "217", null, "555-5678", "101", null, null, null ); @@ -285,7 +285,7 @@ private CustomerDto createFullCustomerDto() { null, null, "customer@example.com", "support@example.com", "https://www.example.com", null, null, null ); - CustomerDto.OrganisationDto organisation = new CustomerDto.OrganisationDto( + OrganisationDto organisation = new OrganisationDto( streetAddress, postalAddress, phone1, phone2, electronicAddress, "ACME Energy Services" ); @@ -312,7 +312,7 @@ private CustomerDto createFullCustomerDto() { } private CustomerDto createMinimalCustomerDto() { - CustomerDto.OrganisationDto organisation = new CustomerDto.OrganisationDto( + OrganisationDto organisation = new OrganisationDto( null, null, null, null, null, "Test Org" ); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDtoTest.java new file mode 100644 index 00000000..b84a5e4e --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/OrganisationDtoTest.java @@ -0,0 +1,219 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for OrganisationDto. + * Verifies DTO structure and field assignments for ESPI 4.0 customer.xsd compliance. + * Organisation schema definition: customer.xsd lines 1096-1125. + */ +@DisplayName("OrganisationDto Unit Tests") +class OrganisationDtoTest { + + @Test + @DisplayName("Should create OrganisationDto with all fields") + void shouldCreateOrganisationDtoWithAllFields() { + // Arrange + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "123 Main St", "Springfield", "IL", "62701", "USA" + ); + + CustomerDto.StreetAddressDto postalAddress = new CustomerDto.StreetAddressDto( + "PO Box 456", "Springfield", "IL", "62702", "USA" + ); + + TelephoneNumberDto phone1 = new TelephoneNumberDto( + "1", "217", null, "555-1234", null, null, null, null + ); + + TelephoneNumberDto phone2 = new TelephoneNumberDto( + "1", "217", null, "555-5678", "101", null, null, null + ); + + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( + null, null, "info@example.com", null, "https://www.example.com", null, null, null + ); + + // Act + OrganisationDto organisation = new OrganisationDto( + streetAddress, postalAddress, phone1, phone2, electronicAddress, "Test Organisation" + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getStreetAddress()).isEqualTo(streetAddress); + assertThat(organisation.getPostalAddress()).isEqualTo(postalAddress); + assertThat(organisation.getPhone1()).isEqualTo(phone1); + assertThat(organisation.getPhone2()).isEqualTo(phone2); + assertThat(organisation.getElectronicAddress()).isEqualTo(electronicAddress); + assertThat(organisation.getOrganisationName()).isEqualTo("Test Organisation"); + } + + @Test + @DisplayName("Should create OrganisationDto with minimal data") + void shouldCreateOrganisationDtoWithMinimalData() { + // Arrange & Act + OrganisationDto organisation = new OrganisationDto( + null, null, null, null, null, "Minimal Org" + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getStreetAddress()).isNull(); + assertThat(organisation.getPostalAddress()).isNull(); + assertThat(organisation.getPhone1()).isNull(); + assertThat(organisation.getPhone2()).isNull(); + assertThat(organisation.getElectronicAddress()).isNull(); + assertThat(organisation.getOrganisationName()).isEqualTo("Minimal Org"); + } + + @Test + @DisplayName("Should create OrganisationDto with street address only") + void shouldCreateOrganisationDtoWithStreetAddressOnly() { + // Arrange + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "789 Business Blvd", "Capital City", "CA", "95814", "USA" + ); + + // Act + OrganisationDto organisation = new OrganisationDto( + streetAddress, null, null, null, null, "Business Org" + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getStreetAddress()).isEqualTo(streetAddress); + assertThat(organisation.getPostalAddress()).isNull(); + assertThat(organisation.getOrganisationName()).isEqualTo("Business Org"); + } + + @Test + @DisplayName("Should create OrganisationDto with phone numbers only") + void shouldCreateOrganisationDtoWithPhoneNumbersOnly() { + // Arrange + TelephoneNumberDto phone1 = new TelephoneNumberDto( + "1", "800", null, "555-0000", null, null, null, null + ); + + TelephoneNumberDto phone2 = new TelephoneNumberDto( + "1", "888", null, "555-1111", null, null, null, null + ); + + // Act + OrganisationDto organisation = new OrganisationDto( + null, null, phone1, phone2, null, "Phone Only Org" + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getPhone1()).isEqualTo(phone1); + assertThat(organisation.getPhone2()).isEqualTo(phone2); + assertThat(organisation.getOrganisationName()).isEqualTo("Phone Only Org"); + } + + @Test + @DisplayName("Should create OrganisationDto with electronic address only") + void shouldCreateOrganisationDtoWithElectronicAddressOnly() { + // Arrange + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( + null, null, "contact@digital.org", null, "https://digital.org", null, null, null + ); + + // Act + OrganisationDto organisation = new OrganisationDto( + null, null, null, null, electronicAddress, "Digital Org" + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getElectronicAddress()).isEqualTo(electronicAddress); + assertThat(organisation.getOrganisationName()).isEqualTo("Digital Org"); + } + + @Test + @DisplayName("Should support null organisation name") + void shouldSupportNullOrganisationName() { + // Arrange & Act + OrganisationDto organisation = new OrganisationDto( + null, null, null, null, null, null + ); + + // Assert + assertThat(organisation).isNotNull(); + assertThat(organisation.getOrganisationName()).isNull(); + } + + @Test + @DisplayName("Should maintain field values after creation") + void shouldMaintainFieldValuesAfterCreation() { + // Arrange + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "555 Test Ave", "TestCity", "TS", "55555", "USA" + ); + + TelephoneNumberDto phone = new TelephoneNumberDto( + "1", "555", "123", "4567", "890", "9", "+", "tel:+15551234567890" + ); + + // Act + OrganisationDto organisation = new OrganisationDto( + streetAddress, null, phone, null, null, "Test Org" + ); + + // Assert - Verify all fields maintain their values + assertThat(organisation.getStreetAddress().getStreetDetail()).isEqualTo("555 Test Ave"); + assertThat(organisation.getStreetAddress().getTownDetail()).isEqualTo("TestCity"); + assertThat(organisation.getStreetAddress().getStateOrProvince()).isEqualTo("TS"); + assertThat(organisation.getStreetAddress().getPostalCode()).isEqualTo("55555"); + assertThat(organisation.getStreetAddress().getCountry()).isEqualTo("USA"); + assertThat(organisation.getPhone1().getCountryCode()).isEqualTo("1"); + assertThat(organisation.getPhone1().getAreaCode()).isEqualTo("555"); + assertThat(organisation.getPhone1().getCityCode()).isEqualTo("123"); + assertThat(organisation.getPhone1().getLocalNumber()).isEqualTo("4567"); + assertThat(organisation.getPhone1().getExt()).isEqualTo("890"); + assertThat(organisation.getOrganisationName()).isEqualTo("Test Org"); + } + + @Test + @DisplayName("Should handle equals and hashCode correctly") + void shouldHandleEqualsAndHashCodeCorrectly() { + // Arrange + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "123 Main St", "City", "ST", "12345", "USA" + ); + + OrganisationDto org1 = new OrganisationDto( + streetAddress, null, null, null, null, "Test" + ); + + OrganisationDto org2 = new OrganisationDto( + streetAddress, null, null, null, null, "Test" + ); + + // Assert + assertThat(org1).isEqualTo(org2); + assertThat(org1.hashCode()).isEqualTo(org2.hashCode()); + } +} \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java index 8e34f20f..6d20e112 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java @@ -304,11 +304,11 @@ private ServiceLocationDto createFullServiceLocationDto() { "PO Box 789", "Chicago", "IL", "60602", "USA" ); - CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone1 = new TelephoneNumberDto( "1", "312", "773", "555-1000", "100", "9", "011", "+1-312-555-1000" ); - CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + TelephoneNumberDto phone2 = new TelephoneNumberDto( "1", "312", "773", "555-2000", "200", "9", "011", "+1-312-555-2000" ); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDtoTest.java new file mode 100644 index 00000000..31beca01 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceSupplierDtoTest.java @@ -0,0 +1,421 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.enums.SupplierKind; +import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; +import org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto; +import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * XML marshalling/unmarshalling tests for ServiceSupplierDto. + * Verifies Jakarta JAXB Marshaller processes JAXB annotations correctly for ESPI 4.0 customer.xsd compliance. + * ServiceSupplier schema definition: customer.xsd lines 1159-1186. + */ +@DisplayName("ServiceSupplierDto XML Marshalling Tests") +class ServiceSupplierDtoTest { + + private DtoExportServiceImpl dtoExportService; + + @BeforeEach + void setUp() { + // Initialize DtoExportService with null repository/mapper (not needed for DTO-only tests) + org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService espiIdGeneratorService = + new org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService(); + dtoExportService = new DtoExportServiceImpl(null, null, espiIdGeneratorService); + } + + @Test + @DisplayName("Should export ServiceSupplier with complete realistic data") + void shouldExportServiceSupplierWithRealisticData() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceSupplierDto serviceSupplier = createFullServiceSupplierDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:550e8400-e29b-51d4-a716-446655440000", + "ACME Utility Company", + now, now, null, serviceSupplier + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "ServiceSupplier Feed", now, now, null, + List.of(entry) + ); + + // Act + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + dtoExportService.exportAtomFeed(feed, stream); + String xml = stream.toString(StandardCharsets.UTF_8); + + // Debug output + System.out.println("========== ServiceSupplier XML Output =========="); + System.out.println(xml); + System.out.println("================================================="); + + // Assert - Basic structure, namespaces, and field values + assertThat(xml) + .startsWith("") + .contains("") + .contains(""); + int issuerPos = xml.indexOf(""); + int effectiveDatePos = xml.indexOf(""); + int postalPos = xml.indexOf(""); + int phone1Pos = xml.indexOf(""); + int phone2Pos = xml.indexOf(""); + int electronicPos = xml.indexOf(""); + int orgNamePos = xml.indexOf(""); + + assertThat(streetPos).isGreaterThan(0).isLessThan(postalPos); + assertThat(postalPos).isLessThan(phone1Pos); + assertThat(phone1Pos).isLessThan(phone2Pos); + assertThat(phone2Pos).isLessThan(electronicPos); + assertThat(electronicPos).isLessThan(orgNamePos); + } + + @Test + @DisplayName("Should export ServiceSupplier with minimal data") + void shouldExportServiceSupplierWithMinimalData() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "123 Minimal St", "City", "ST", "12345", "USA" + ); + + OrganisationDto organisation = new OrganisationDto( + streetAddress, null, null, null, null, "Minimal Supplier" + ); + + ServiceSupplierDto serviceSupplier = new ServiceSupplierDto( + organisation, + SupplierKind.UTILITY, + null, null + ); + + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:550e8400-e29b-51d4-a716-446655440002", + "Minimal ServiceSupplier", + now, now, null, serviceSupplier + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "Test Feed", now, now, null, + List.of(entry) + ); + + // Act + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + dtoExportService.exportAtomFeed(feed, stream); + String xml = stream.toString(StandardCharsets.UTF_8); + + // Assert + assertThat(xml) + .contains("") + .contains("1") + .contains("555") + .contains("1234") + .contains("") + .contains("5678"); + } + + @Test + @DisplayName("Should verify TelephoneNumber field order per customer.xsd") + void shouldVerifyTelephoneNumberFieldOrder() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceSupplierDto serviceSupplier = createFullServiceSupplierDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:550e8400-e29b-51d4-a716-446655440006", + "Test ServiceSupplier", + now, now, null, serviceSupplier + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "Test Feed", now, now, null, + List.of(entry) + ); + + // Act + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + dtoExportService.exportAtomFeed(feed, stream); + String xml = stream.toString(StandardCharsets.UTF_8); + + // Assert - Verify TelephoneNumber field order per customer.xsd:1428-1478 + // Order: countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone + int phone1StartPos = xml.indexOf(""); + int phone1EndPos = xml.indexOf(""); + + String phone1Section = xml.substring(phone1StartPos, phone1EndPos); + + int countryCodePos = phone1Section.indexOf(""); + int areaCodePos = phone1Section.indexOf(""); + int localNumberPos = phone1Section.indexOf(""); + + assertThat(countryCodePos).isGreaterThan(0).isLessThan(areaCodePos); + assertThat(areaCodePos).isLessThan(localNumberPos); + } + + @Test + @DisplayName("Should export ServiceSupplier with all SupplierKind values") + void shouldExportServiceSupplierWithAllSupplierKindValues() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + // Test each SupplierKind enum value + SupplierKind[] kinds = { + SupplierKind.UTILITY, + SupplierKind.RETAILER, + SupplierKind.OTHER + }; + + for (SupplierKind kind : kinds) { + ServiceSupplierDto serviceSupplier = createMinimalServiceSupplierDto(); + // Update kind field via new instance since DTO is immutable + serviceSupplier = new ServiceSupplierDto( + serviceSupplier.getOrganisation(), + kind, + serviceSupplier.getIssuerIdentificationNumber(), + serviceSupplier.getEffectiveDate() + ); + + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:test-kind-" + kind, "Test", now, now, null, serviceSupplier + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "Test Feed", now, now, null, + List.of(entry) + ); + + // Act + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + dtoExportService.exportAtomFeed(feed, stream); + String xml = stream.toString(StandardCharsets.UTF_8); + + // Assert + assertThat(xml).contains(kind.toString()); + } + } + + // Helper methods + + private ServiceSupplierDto createFullServiceSupplierDto() { + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "456 Utility Ave", "Metropolis", "NY", "10001", "USA" + ); + + CustomerDto.StreetAddressDto postalAddress = new CustomerDto.StreetAddressDto( + "PO Box 789", "Metropolis", "NY", "10002", "USA" + ); + + TelephoneNumberDto phone1 = new TelephoneNumberDto( + "1", "555", null, "1234", null, null, null, null + ); + + TelephoneNumberDto phone2 = new TelephoneNumberDto( + "1", "555", null, "5678", "200", null, null, null + ); + + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( + null, null, "info@acmeutility.com", "support@acmeutility.com", + "https://www.acmeutility.com", null, null, null + ); + + OrganisationDto organisation = new OrganisationDto( + streetAddress, postalAddress, phone1, phone2, electronicAddress, "ACME Utility Company" + ); + + return new ServiceSupplierDto( + organisation, + SupplierKind.UTILITY, + "ISS-123456789", + OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) + ); + } + + private ServiceSupplierDto createMinimalServiceSupplierDto() { + CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto( + "123 Test St", "City", "ST", "12345", "USA" + ); + + OrganisationDto organisation = new OrganisationDto( + streetAddress, null, null, null, null, "Test Supplier Org" + ); + + return new ServiceSupplierDto( + organisation, + SupplierKind.UTILITY, + null, null + ); + } +} diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDtoTest.java new file mode 100644 index 00000000..7eb129eb --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/TelephoneNumberDtoTest.java @@ -0,0 +1,201 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for TelephoneNumberDto. + * Verifies DTO structure and field assignments for ESPI 4.0 customer.xsd compliance. + * TelephoneNumber schema definition: customer.xsd lines 1428-1478. + */ +@DisplayName("TelephoneNumberDto Unit Tests") +class TelephoneNumberDtoTest { + + @Test + @DisplayName("Should create TelephoneNumberDto with all fields") + void shouldCreateTelephoneNumberDtoWithAllFields() { + // Arrange & Act + TelephoneNumberDto phone = new TelephoneNumberDto( + "1", // countryCode + "555", // areaCode + "123", // cityCode + "4567", // localNumber + "890", // ext + "9", // dialOut + "+", // internationalPrefix + "tel:+15551234567890" // ituPhone + ); + + // Assert + assertThat(phone).isNotNull() + .extracting( + TelephoneNumberDto::getCountryCode, + TelephoneNumberDto::getAreaCode, + TelephoneNumberDto::getCityCode, + TelephoneNumberDto::getLocalNumber, + TelephoneNumberDto::getExt, + TelephoneNumberDto::getDialOut, + TelephoneNumberDto::getInternationalPrefix, + TelephoneNumberDto::getItuPhone + ) + .containsExactly("1", "555", "123", "4567", "890", "9", "+", "tel:+15551234567890"); + } + + @Test + @DisplayName("Should create TelephoneNumberDto with minimal data") + void shouldCreateTelephoneNumberDtoWithMinimalData() { + // Arrange & Act + TelephoneNumberDto phone = new TelephoneNumberDto( + null, null, null, "555-1234", null, null, null, null + ); + + // Assert + assertThat(phone).isNotNull(); + assertThat(phone.getLocalNumber()).isEqualTo("555-1234"); + assertThat(phone.getCountryCode()).isNull(); + assertThat(phone.getAreaCode()).isNull(); + assertThat(phone.getCityCode()).isNull(); + assertThat(phone.getExt()).isNull(); + assertThat(phone.getDialOut()).isNull(); + assertThat(phone.getInternationalPrefix()).isNull(); + assertThat(phone.getItuPhone()).isNull(); + } + + @Test + @DisplayName("Should create TelephoneNumberDto with standard US format") + void shouldCreateTelephoneNumberDtoWithStandardUSFormat() { + // Arrange & Act - Standard US phone: +1 (555) 123-4567 + TelephoneNumberDto phone = new TelephoneNumberDto( + "1", "555", null, "123-4567", null, null, null, null + ); + + // Assert + assertThat(phone).isNotNull(); + assertThat(phone.getCountryCode()).isEqualTo("1"); + assertThat(phone.getAreaCode()).isEqualTo("555"); + assertThat(phone.getLocalNumber()).isEqualTo("123-4567"); + } + + @Test + @DisplayName("Should create TelephoneNumberDto with extension") + void shouldCreateTelephoneNumberDtoWithExtension() { + // Arrange & Act + TelephoneNumberDto phone = new TelephoneNumberDto( + "1", "555", null, "1234", "567", null, null, null + ); + + // Assert + assertThat(phone).isNotNull(); + assertThat(phone.getLocalNumber()).isEqualTo("1234"); + assertThat(phone.getExt()).isEqualTo("567"); + } + + @Test + @DisplayName("Should create TelephoneNumberDto with international format") + void shouldCreateTelephoneNumberDtoWithInternationalFormat() { + // Arrange & Act - International: +44 20 7946 0958 + TelephoneNumberDto phone = new TelephoneNumberDto( + "44", // UK country code + "20", // London area code + null, + "7946 0958", // local number + null, + "00", // dialOut for international + "+", // international prefix + "tel:+442079460958" + ); + + // Assert + assertThat(phone).isNotNull(); + assertThat(phone.getCountryCode()).isEqualTo("44"); + assertThat(phone.getAreaCode()).isEqualTo("20"); + assertThat(phone.getLocalNumber()).isEqualTo("7946 0958"); + assertThat(phone.getDialOut()).isEqualTo("00"); + assertThat(phone.getInternationalPrefix()).isEqualTo("+"); + assertThat(phone.getItuPhone()).isEqualTo("tel:+442079460958"); + } + + @Test + @DisplayName("Should support null values for all fields") + void shouldSupportNullValuesForAllFields() { + // Arrange & Act + TelephoneNumberDto phone = new TelephoneNumberDto( + null, null, null, null, null, null, null, null + ); + + // Assert + assertThat(phone).isNotNull(); + assertThat(phone.getCountryCode()).isNull(); + assertThat(phone.getAreaCode()).isNull(); + assertThat(phone.getCityCode()).isNull(); + assertThat(phone.getLocalNumber()).isNull(); + assertThat(phone.getExt()).isNull(); + assertThat(phone.getDialOut()).isNull(); + assertThat(phone.getInternationalPrefix()).isNull(); + assertThat(phone.getItuPhone()).isNull(); + } + + @Test + @DisplayName("Should maintain field values after creation") + void shouldMaintainFieldValuesAfterCreation() { + // Arrange & Act + TelephoneNumberDto phone = new TelephoneNumberDto( + "1", "800", "555", "1212", "123", "9", "+1", "tel:+18005551212" + ); + + // Assert - Access each field multiple times to verify immutability + assertThat(phone.getCountryCode()).isEqualTo("1"); + assertThat(phone.getCountryCode()).isEqualTo("1"); // Second access + assertThat(phone.getAreaCode()).isEqualTo("800"); + assertThat(phone.getAreaCode()).isEqualTo("800"); + assertThat(phone.getCityCode()).isEqualTo("555"); + assertThat(phone.getLocalNumber()).isEqualTo("1212"); + assertThat(phone.getExt()).isEqualTo("123"); + assertThat(phone.getDialOut()).isEqualTo("9"); + assertThat(phone.getInternationalPrefix()).isEqualTo("+1"); + assertThat(phone.getItuPhone()).isEqualTo("tel:+18005551212"); + } + + @Test + @DisplayName("Should handle equals and hashCode correctly") + void shouldHandleEqualsAndHashCodeCorrectly() { + // Arrange + TelephoneNumberDto phone1 = new TelephoneNumberDto( + "1", "555", null, "1234", null, null, null, null + ); + + TelephoneNumberDto phone2 = new TelephoneNumberDto( + "1", "555", null, "1234", null, null, null, null + ); + + TelephoneNumberDto phone3 = new TelephoneNumberDto( + "1", "555", null, "5678", null, null, null, null + ); + + // Assert + assertThat(phone1).isEqualTo(phone2); + assertThat(phone1.hashCode()).isEqualTo(phone2.hashCode()); + assertThat(phone1).isNotEqualTo(phone3); + } +} \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java index 58ef86ed..7444065b 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java @@ -70,18 +70,6 @@ private ServiceLocationEntity createValidServiceLocation() { return serviceLocation; } - /** - * Creates a valid PhoneNumberEntity for testing. - */ - private PhoneNumberEntity createValidPhoneNumber() { - PhoneNumberEntity phoneNumber = new PhoneNumberEntity(); - phoneNumber.setAreaCode(faker.number().digits(3)); - phoneNumber.setCityCode(faker.number().digits(3)); - phoneNumber.setLocalNumber(faker.number().digits(4)); - phoneNumber.setParentEntityType("ServiceLocationEntity"); - return phoneNumber; - } - @Nested @DisplayName("CRUD Operations") class CrudOperationsTest {