From a94e8d3f159d3fb837da90acf9526b03abcadaf8 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Tue, 27 Jan 2026 16:22:53 -0500 Subject: [PATCH] feat: ESPI 4.0 Schema Compliance - Phase 23 ServiceLocation Complete Implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements full ESPI 4.0 customer.xsd compliance for ServiceLocation including Location inheritance (type → mainAddress → phone1/phone2 → status → positionPoints) and ServiceLocation fields (accessMethod → usagePointHrefs → outageBlock). ## Entity Changes - ServiceLocationEntity: Added status.remark @AttributeOverride, usagePointHrefs collection - ServiceLocationEntity: Replaced PhoneNumberEntity with embedded TelephoneNumber (8 fields) - Organisation: Created TelephoneNumber @Embeddable with all 8 customer.xsd fields - PhoneNumberEntity: Marked @Deprecated (retained for backward compatibility) ## DTO Changes - ServiceLocationDto: Complete refactor to match customer.xsd ServiceLocation → WorkLocation → Location - ServiceLocationDto: Added all Location fields (type, addresses, phone1/phone2, electronicAddress, geoInfoReference, direction, status, positionPoints) - ServiceLocationDto: Added all ServiceLocation fields (accessMethod, siteAccessProblem, needsInspection, usagePointHrefs, outageBlock) - CustomerDto: Renamed PhoneNumberDto → TelephoneNumberDto with 8 fields - StatusDto: Created shared top-level DTO for Status type (value, dateTime, remark, reason) - CustomerDto: Removed nested StatusDto class (uses shared StatusDto) ## Mapper Changes - ServiceLocationMapper: Created complete bidirectional Entity ↔ DTO mapper - CustomerMapper: Updated for 8-field TelephoneNumber - DtoExportServiceImpl: Added StatusDto to JAXBContext initialization ## Repository/Service Changes - ServiceLocationRepository: Removed 5 non-indexed query methods for performance - ServiceLocationService/Impl: Removed corresponding service methods ## Database Migration - V3__Create_additiional_Base_Tables.sql: Added status_remark column - V3__Create_additiional_Base_Tables.sql: Added 16 phone columns (phone1_*, phone2_*) - V3__Create_additiional_Base_Tables.sql: Created service_location_usage_point_hrefs table ## Test Changes - ServiceLocationDtoTest: Created comprehensive 6-test suite for XML marshalling - ServiceLocationRepositoryTest: Removed tests for deleted query methods - CustomerDtoTest/MarshallingTest: Updated for 8-field TelephoneNumber and shared StatusDto - All XML assertion tests: Updated to chain assertions and handle self-closing tags ## JAXB Fixes - Resolved "Two classes have same XML type name Status" IllegalAnnotationsException - Created shared StatusDto to eliminate duplicate nested classes - Fixed JAXBContext initialization to include shared StatusDto ## Test Results - All 636 tests pass (0 failures, 0 errors) - Integration tests pass - Full CI/CD verification complete ## Schema Compliance - ServiceLocation per customer.xsd lines 1218-1280 - Location per customer.xsd lines 914-997 - TelephoneNumber per customer.xsd lines 1428-1478 - Status per customer.xsd lines 1254-1280 Co-Authored-By: Claude Sonnet 4.5 --- .github/agents/code-reviewer.md | 13 + .github/agents/github-checker.md | 16 + .github/agents/test-writer.md | 14 + ..._18_CUSTOMERACCOUNT_IMPLEMENTATION_PLAN.md | 1019 ++++++++++++++++ PHASE_20_CUSTOMER_IMPLEMENTATION_PLAN.md | 370 ++++++ PHASE_23_EXPANDED_SCOPE.md | 161 +++ PHASE_23_PROGRESS_CHECKPOINT.md | 249 ++++ ..._23_SERVICELOCATION_IMPLEMENTATION_PLAN.md | 541 +++++++++ PHASE_23_TASK_BREAKDOWN.md | 1044 +++++++++++++++++ ...4_CUSTOMERAGREEMENT_IMPLEMENTATION_PLAN.md | 783 +++++++++++++ PHASE_24_TASK_BREAKDOWN.md | 1003 ++++++++++++++++ customer-dto-current-output.xml | 65 + intervalblock-dto-output.xml | 62 + .../domain/customer/entity/Organisation.java | 70 +- .../customer/entity/PhoneNumberEntity.java | 40 +- .../entity/ServiceLocationEntity.java | 116 +- .../espi/common/dto/customer/CustomerDto.java | 49 +- .../dto/customer/ServiceLocationDto.java | 179 +-- .../espi/common/dto/customer/StatusDto.java | 67 ++ .../mapper/customer/CustomerMapper.java | 23 +- .../customer/ServiceLocationMapper.java | 246 ++++ .../customer/ServiceLocationRepository.java | 47 +- .../customer/ServiceLocationService.java | 35 - .../impl/ServiceLocationServiceImpl.java | 42 - .../service/impl/DtoExportServiceImpl.java | 1 + .../V3__Create_additiional_Base_Tables.sql | 37 +- .../customer/CustomerDtoMarshallingTest.java | 19 +- .../common/dto/customer/CustomerDtoTest.java | 13 +- .../dto/customer/ServiceLocationDtoTest.java | 374 ++++++ .../ServiceLocationRepositoryTest.java | 125 +- 30 files changed, 6425 insertions(+), 398 deletions(-) create mode 100644 .github/agents/code-reviewer.md create mode 100644 .github/agents/github-checker.md create mode 100644 .github/agents/test-writer.md create mode 100644 PHASE_18_CUSTOMERACCOUNT_IMPLEMENTATION_PLAN.md create mode 100644 PHASE_20_CUSTOMER_IMPLEMENTATION_PLAN.md create mode 100644 PHASE_23_EXPANDED_SCOPE.md create mode 100644 PHASE_23_PROGRESS_CHECKPOINT.md create mode 100644 PHASE_23_SERVICELOCATION_IMPLEMENTATION_PLAN.md create mode 100644 PHASE_23_TASK_BREAKDOWN.md create mode 100644 PHASE_24_CUSTOMERAGREEMENT_IMPLEMENTATION_PLAN.md create mode 100644 PHASE_24_TASK_BREAKDOWN.md create mode 100644 customer-dto-current-output.xml create mode 100644 intervalblock-dto-output.xml create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatusDto.java create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java create mode 100644 openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java diff --git a/.github/agents/code-reviewer.md b/.github/agents/code-reviewer.md new file mode 100644 index 00000000..2e1d4082 --- /dev/null +++ b/.github/agents/code-reviewer.md @@ -0,0 +1,13 @@ +--- +name: code-reviewer +description: Reviews code for issues. Use after making changes. +tools: Read, Grep, Glob +model: sonnet +--- +​ +You are a code reviewer. When invoked, review the most recent changes for: +- Bugs or logic errors +- Missing error handling +- Code style issues + ​ + Be concise. Prioritise by severity. \ No newline at end of file diff --git a/.github/agents/github-checker.md b/.github/agents/github-checker.md new file mode 100644 index 00000000..f533b27e --- /dev/null +++ b/.github/agents/github-checker.md @@ -0,0 +1,16 @@ +--- +name: github-checker +description: Checks GitHub API for repository information. Use for project stats. +tools: Bash +model: haiku +--- +​ +You check GitHub repositories using the public API. +​ +When given a repo (owner/name format): +1. Fetch the repo info using curl +2. Extract: stars, forks, last updated, description +3. Return a clean summary + ​ + Example API call: + curl -s "https://api.github.com/repos/anthropics/claude-code" \ No newline at end of file diff --git a/.github/agents/test-writer.md b/.github/agents/test-writer.md new file mode 100644 index 00000000..7db0af22 --- /dev/null +++ b/.github/agents/test-writer.md @@ -0,0 +1,14 @@ +--- +name: test-writer +description: Writes tests for new code. Use proactively after features. +tools: Read, Write, Edit, Bash +model: sonnet +--- +​ +You write tests. When given a feature or module: +1. Analyse the code +2. Identify edge cases +3. Write comprehensive tests +4. Run them to verify they pass + ​ + Focus on behaviour, not implementation details. \ No newline at end of file diff --git a/PHASE_18_CUSTOMERACCOUNT_IMPLEMENTATION_PLAN.md b/PHASE_18_CUSTOMERACCOUNT_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..b5b03b19 --- /dev/null +++ b/PHASE_18_CUSTOMERACCOUNT_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1019 @@ +# Phase 18: CustomerAccount - ESPI 4.0 Schema Compliance Implementation Plan + +**Branch**: `feature/schema-compliance-phase-18-customer-account` + +**Target**: Full compliance with NAESB ESPI 4.0 customer.xsd CustomerAccount element sequence + +--- + +## Overview + +CustomerAccount is a customer domain entity that extends Document (which extends IdentifiedObject). It represents an assignment of products and services purchased by a customer through a customer agreement, used for billing and payment. + +### XSD Schema Structure + +**customer.xsd CustomerAccount definition (lines 118-158)**: +```xml + + + + + + + + + + + + + + +``` + +**customer.xsd Document base class (lines 819-872)**: +```xml + + + + + + + + + + + + + + + + + +``` + +### Complete Field Order (IdentifiedObject → Document → CustomerAccount) + +1. **IdentifiedObject fields** (inherited): + - mRID (UUID in entity) + - description + - published + - selfLink + - upLink + - relatedLinks + +2. **Document fields** (inherited): + - type + - authorName + - createdDateTime + - lastModifiedDateTime + - revisionNumber + - electronicAddress + - subject + - title + - docStatus + +3. **CustomerAccount fields** (specific): + - billingCycle + - budgetBill + - lastBillAmount + - notifications (collection of AccountNotification) + - contactInfo (Organisation embedded object) + - accountId + +--- + +## Current State Analysis + +### Issues Found + +#### CustomerAccountEntity.java (openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java) + +**Missing Document Fields**: +- ❌ `authorName` - Missing entirely +- ❌ `electronicAddress` - Missing (ElectronicAddress embedded type) +- ❌ `docStatus` - Missing (Status embedded type) + +**Incorrect Field Types**: +- ❌ Line 130: `contactInfo` is String, should be Organisation embedded object +- ❌ Line 143: `isPrePay` is a custom field not in XSD (extension field, needs to be at end) + +**Field Order Issues**: +- ❌ Document fields (lines 64-95) are not in XSD sequence order +- ❌ CustomerAccount fields (lines 103-136) are not in XSD sequence order +- ❌ Customer relationship field should not be in entity (relationship field, not XML serialized) + +#### CustomerAccountDto.java (openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java) + +**Critical Issues**: +- ❌ Line 41-45: `propOrder` is completely wrong - doesn't match XSD element sequence +- ❌ Missing all Document base class fields except description +- ❌ Line 78-93: Fields are not in correct XSD order +- ❌ Line 80: `accountNumber` field doesn't exist in XSD (should be removed or marked as extension) +- ❌ Line 92: `transactionDate` field doesn't exist in XSD (should be removed) +- ❌ Line 98-103: Customer and CustomerAgreement relationships should not be embedded in DTO + +**Missing Fields**: +- ❌ All Document fields: type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus +- ❌ CustomerAccount.contactInfo should be Organisation type, not String +- ❌ CustomerAccount.notifications collection + +--- + +## Task 1: Entity Updates + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java` + +### Required Changes + +1. **Add Missing Document Fields** (after line 95, before CustomerAccount fields): +```java +/** + * Name of the author of this document. + */ +@Column(name = "author_name", length = 256) +private String authorName; + +/** + * Electronic address. + */ +@Embedded +@AttributeOverrides({ + @AttributeOverride(name = "email1", column = @Column(name = "electronic_address_email1", length = 256)), + @AttributeOverride(name = "email2", column = @Column(name = "electronic_address_email2", length = 256)), + @AttributeOverride(name = "web", column = @Column(name = "electronic_address_web", length = 256)), + @AttributeOverride(name = "radio", column = @Column(name = "electronic_address_radio", length = 256)), + @AttributeOverride(name = "landLineNumber", column = @Column(name = "electronic_address_land_line", length = 256)), + @AttributeOverride(name = "mobileNumber", column = @Column(name = "electronic_address_mobile", length = 256)) +}) +private ElectronicAddress electronicAddress; + +/** + * Status of this document. For status of subject matter this document represents + * (e.g., Agreement, Work), use 'status' attribute. + */ +@Embedded +@AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "doc_status_value", length = 256)), + @AttributeOverride(name = "dateTime", column = @Column(name = "doc_status_date_time")), + @AttributeOverride(name = "reason", column = @Column(name = "doc_status_reason", length = 256)), + @AttributeOverride(name = "remark", column = @Column(name = "doc_status_remark", length = 256)) +}) +private Status docStatus; +``` + +2. **Reorder Document Fields** (lines 59-95) to match XSD sequence: +```java +// Document fields (in XSD order) + +/** + * Type of this document. + */ +@Column(name = "document_type", length = 256) +private String type; + +/** + * Name of the author of this document. + */ +@Column(name = "author_name", length = 256) +private String authorName; + +/** + * Date and time that this document was created. + */ +@Column(name = "created_date_time") +private OffsetDateTime createdDateTime; + +/** + * Date and time that this document was last modified. + */ +@Column(name = "last_modified_date_time") +private OffsetDateTime lastModifiedDateTime; + +/** + * Revision number for this document. + */ +@Column(name = "revision_number", length = 256) +private String revisionNumber; + +/** + * Electronic address. + */ +@Embedded +@AttributeOverrides({...}) +private ElectronicAddress electronicAddress; + +/** + * Subject of this document. + */ +@Column(name = "subject", length = 256) +private String subject; + +/** + * Title of this document. + */ +@Column(name = "title", length = 256) +private String title; + +/** + * Status of this document. + */ +@Embedded +@AttributeOverrides({...}) +private Status docStatus; +``` + +3. **Fix contactInfo Field Type** (line 129-130): +```java +// BEFORE (WRONG): +@Column(name = "contact_name", length = 256) +private String contactInfo; + +// AFTER (CORRECT): +/** + * [extension] Customer contact information used to identify individual + * responsible for billing and payment of CustomerAccount. + */ +@Embedded +@AttributeOverrides({ + @AttributeOverride(name = "organisationName", column = @Column(name = "contact_org_name", length = 256)), + @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "contact_street_detail", length = 256)), + @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "contact_town_detail", length = 256)), + @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "contact_state_province", length = 256)), + @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "contact_postal_code", length = 256)), + @AttributeOverride(name = "streetAddress.country", column = @Column(name = "contact_country", length = 256)), + @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "contact_postal_street_detail", length = 256)), + @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "contact_postal_town_detail", length = 256)), + @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "contact_postal_state_province", length = 256)), + @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "contact_postal_postal_code", length = 256)), + @AttributeOverride(name = "postalAddress.country", column = @Column(name = "contact_postal_country", length = 256)), + @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "contact_email1", length = 256)), + @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "contact_email2", length = 256)), + @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "contact_web", length = 256)) +}) +private Organisation contactInfo; +``` + +4. **Reorder CustomerAccount Fields** (lines 97-136) to match XSD sequence: +```java +// CustomerAccount specific fields (in XSD order) + +/** + * Cycle day on which the associated customer account will normally be billed. + */ +@Column(name = "billing_cycle", length = 256) +private String billingCycle; + +/** + * Budget bill code. + */ +@Column(name = "budget_bill", length = 256) +private String budgetBill; + +/** + * The last amount that will be billed to the customer prior to shut off of the account. + */ +@Column(name = "last_bill_amount") +private Long lastBillAmount; + +/** + * Set of customer account notifications. + */ +@ElementCollection(fetch = FetchType.LAZY) +@CollectionTable(name = "customer_account_notifications", joinColumns = @JoinColumn(name = "customer_account_id")) +private List notifications; + +/** + * [extension] Customer contact information. + */ +@Embedded +@AttributeOverrides({...}) +private Organisation contactInfo; + +/** + * [extension] Customer account identifier. + */ +@Column(name = "account_id", length = 256) +private String accountId; + +// Extension fields (not in XSD, must be at end) + +/** + * [extension] Indicates whether this customer account is a prepaid account. + */ +@Column(name = "is_pre_pay") +private Boolean isPrePay; + +// Relationship fields (not serialized to XML) + +/** + * Customer that owns this account. + */ +@ManyToOne(fetch = FetchType.LAZY) +@JoinColumn(name = "customer_id") +private CustomerEntity customer; +``` + +5. **Update toString()** method to include new fields in correct order + +### Verification Checklist for Entity + +- [ ] All Document base class fields present (type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus) +- [ ] Document fields in correct XSD sequence +- [ ] All CustomerAccount fields present (billingCycle, budgetBill, lastBillAmount, notifications, contactInfo, accountId) +- [ ] CustomerAccount fields in correct XSD sequence +- [ ] contactInfo is Organisation type (not String) +- [ ] Extension fields (isPrePay) at end with [extension] comment +- [ ] Relationship fields (customer) at end +- [ ] JPA annotations correct (@Embedded, @AttributeOverrides, @ElementCollection) +- [ ] Column names follow snake_case convention +- [ ] toString() includes all fields in order + +--- + +## Task 2: DTO Updates + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java` + +### Required Changes + +1. **Completely Rewrite @XmlType propOrder** (line 41-45): +```java +@XmlType(name = "CustomerAccount", namespace = "http://naesb.org/espi/customer", propOrder = { + // IdentifiedObject fields + "description", + // Document fields + "type", + "authorName", + "createdDateTime", + "lastModifiedDateTime", + "revisionNumber", + "electronicAddress", + "subject", + "title", + "docStatus", + // CustomerAccount fields + "billingCycle", + "budgetBill", + "lastBillAmount", + "notifications", + "contactInfo", + "accountId" +}) +``` + +2. **Add Missing Document Fields** (after description, before current CustomerAccount fields): +```java +@XmlElement(name = "type") +private String type; + +@XmlElement(name = "authorName") +private String authorName; + +@XmlElement(name = "createdDateTime") +private OffsetDateTime createdDateTime; + +@XmlElement(name = "lastModifiedDateTime") +private OffsetDateTime lastModifiedDateTime; + +@XmlElement(name = "revisionNumber") +private String revisionNumber; + +@XmlElement(name = "electronicAddress") +private ElectronicAddress electronicAddress; + +@XmlElement(name = "subject") +private String subject; + +@XmlElement(name = "title") +private String title; + +@XmlElement(name = "docStatus") +private Status docStatus; +``` + +3. **Fix contactInfo Field Type** (line 77): +```java +// BEFORE (WRONG): +// contactInfo field doesn't exist in current DTO + +// AFTER (CORRECT): +@XmlElement(name = "contactInfo") +private Organisation contactInfo; +``` + +4. **Remove Non-XSD Fields**: +```java +// REMOVE these lines: +@XmlElement(name = "accountNumber") +private String accountNumber; + +@XmlElement(name = "transactionDate") +private OffsetDateTime transactionDate; + +@XmlElement(name = "Customer") +private CustomerDto customer; + +@XmlElement(name = "CustomerAgreement") +@XmlElementWrapper(name = "CustomerAgreements") +private List customerAgreements; +``` + +5. **Add notifications Collection**: +```java +@XmlElement(name = "AccountNotification") +@XmlElementWrapper(name = "notifications") +private List notifications; +``` + +6. **Update Constructors** to match new field list + +7. **Update Helper Methods** (getSelfHref, getUpHref, generateSelfHref, generateUpHref) + +### Verification Checklist for DTO + +- [ ] @XmlType propOrder matches exact XSD element sequence +- [ ] All IdentifiedObject fields present (mRID as uuid, description) +- [ ] All Document fields present (type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus) +- [ ] All CustomerAccount fields present (billingCycle, budgetBill, lastBillAmount, notifications, contactInfo, accountId) +- [ ] contactInfo is Organisation type +- [ ] No non-XSD fields (accountNumber, transactionDate removed) +- [ ] No relationship objects embedded (Customer, CustomerAgreements removed) +- [ ] JAXB annotations correct (@XmlElement with correct names) +- [ ] Constructors updated +- [ ] Helper methods work correctly + +--- + +## Task 3: MapStruct Mapper Updates + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java` + +### Required Changes + +1. **Add Missing Field Mappings**: +```java +@Mapping(source = "authorName", target = "authorName") +@Mapping(source = "electronicAddress", target = "electronicAddress") +@Mapping(source = "docStatus", target = "docStatus") +@Mapping(source = "contactInfo", target = "contactInfo") +@Mapping(source = "notifications", target = "notifications") +``` + +2. **Update toDto() Method**: +```java +@Mapping(source = "id", target = "id") +@Mapping(source = "uuid", target = "uuid") +@Mapping(source = "description", target = "description") +@Mapping(source = "published", target = "published") +@Mapping(source = "type", target = "type") +@Mapping(source = "authorName", target = "authorName") +@Mapping(source = "createdDateTime", target = "createdDateTime") +@Mapping(source = "lastModifiedDateTime", target = "lastModifiedDateTime") +@Mapping(source = "revisionNumber", target = "revisionNumber") +@Mapping(source = "electronicAddress", target = "electronicAddress") +@Mapping(source = "subject", target = "subject") +@Mapping(source = "title", target = "title") +@Mapping(source = "docStatus", target = "docStatus") +@Mapping(source = "billingCycle", target = "billingCycle") +@Mapping(source = "budgetBill", target = "budgetBill") +@Mapping(source = "lastBillAmount", target = "lastBillAmount") +@Mapping(source = "notifications", target = "notifications") +@Mapping(source = "contactInfo", target = "contactInfo") +@Mapping(source = "accountId", target = "accountId") +@Mapping(target = "selfLink", ignore = true) +@Mapping(target = "upLink", ignore = true) +@Mapping(target = "relatedLinks", ignore = true) +CustomerAccountDto toDto(CustomerAccountEntity entity); +``` + +3. **Update toEntity() Method** with corresponding reverse mappings + +4. **Handle Embedded Object Mappings**: + - ElectronicAddress mapping + - Organisation mapping + - Status mapping + - AccountNotification collection mapping + +### Verification Checklist for Mapper + +- [ ] All Document fields mapped (both directions) +- [ ] All CustomerAccount fields mapped (both directions) +- [ ] Embedded objects map correctly (ElectronicAddress, Organisation, Status) +- [ ] Collections map correctly (notifications) +- [ ] Link fields handled correctly (selfLink, upLink, relatedLinks) +- [ ] No unmapped target property warnings in build +- [ ] MapStruct generated code compiles + +--- + +## Task 4: Repository Updates + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepository.java` + +### Required Changes + +1. **Review Existing Queries**: + - Keep ONLY queries using indexed fields + - Remove any custom queries not required for tests + +2. **Standard JpaRepository Methods** (should remain): +```java +public interface CustomerAccountRepository extends JpaRepository { + // Standard methods: findById, findAll, save, delete, count, existsById + + // Custom queries ONLY if field is indexed +} +``` + +### Current Indexed Fields (from Flyway migration) +- `id` (primary key, UUID) +- `customer_id` (foreign key to customer) +- `account_id` (customer account identifier) +- `billing_cycle` + +### Verification Checklist for Repository + +- [ ] Extends JpaRepository +- [ ] Only indexed field queries present +- [ ] No complex queries that would be better in service layer +- [ ] All query methods have proper return types + +--- + +## Task 5: Service Updates + +**Files**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAccountService.java` +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAccountServiceImpl.java` + +### Required Changes + +1. **Review Service Interface** (CustomerAccountService.java): + - Basic CRUD operations + - Any schema-compliant business logic + - No complex queries better suited for repository + +2. **Review Service Implementation** (CustomerAccountServiceImpl.java): + - Field access order matches schema sequence + - Validation uses schema constraints + - Proper handling of embedded objects + +3. **Add Validation Methods** (if needed): +```java +public void validateCustomerAccount(CustomerAccountEntity account) { + // Validate Document fields + // Validate CustomerAccount fields + // Validate embedded objects (electronicAddress, contactInfo, docStatus) + // Validate notifications collection +} +``` + +### Verification Checklist for Service + +- [ ] Service methods work with new Document fields +- [ ] Embedded objects handled correctly in service logic +- [ ] Collections handled correctly (notifications) +- [ ] Validation aligns with XSD constraints +- [ ] All service tests pass + +--- + +## Task 6: Flyway Migration Updates + +**Primary File**: `openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +### Required Changes + +1. **Add Missing Document Fields to customer_accounts Table**: +```sql +-- Add after title column (around line 135) +ALTER TABLE customer_accounts ADD COLUMN author_name VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_email1 VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_email2 VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_web VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_radio VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_land_line VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN electronic_address_mobile VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN doc_status_value VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN doc_status_date_time TIMESTAMP WITH TIME ZONE; +ALTER TABLE customer_accounts ADD COLUMN doc_status_reason VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN doc_status_remark VARCHAR(256); +``` + +2. **Fix contactInfo Column Structure**: +```sql +-- Replace contact_name with Organisation embedded fields +ALTER TABLE customer_accounts DROP COLUMN contact_name; + +ALTER TABLE customer_accounts ADD COLUMN contact_org_name VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_street_detail VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_town_detail VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_state_province VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_code VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_country VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_street_detail VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_town_detail VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_state_province VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_postal_code VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_postal_country VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_email1 VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_email2 VARCHAR(256); +ALTER TABLE customer_accounts ADD COLUMN contact_web VARCHAR(256); +``` + +3. **Verify Column Order Matches XSD**: +```sql +-- Ensure columns are in this order (Document → CustomerAccount): +-- type, author_name, created_date_time, last_modified_date_time, revision_number, +-- electronic_address_*, subject, title, doc_status_*, +-- billing_cycle, budget_bill, last_bill_amount, (notifications in separate table), +-- contact_*, account_id, is_pre_pay +``` + +4. **Verify customer_account_notifications Table** exists and is correct + +5. **Update Indexes** if needed: +```sql +CREATE INDEX IF NOT EXISTS idx_customer_account_billing_cycle ON customer_accounts (billing_cycle); +CREATE INDEX IF NOT EXISTS idx_customer_account_account_id ON customer_accounts (account_id); +``` + +### Verification Checklist for Migration + +- [ ] All Document columns present +- [ ] All CustomerAccount columns present +- [ ] contactInfo as Organisation embedded (not String) +- [ ] Column order matches XSD element sequence +- [ ] customer_account_notifications table correct +- [ ] Indexes created on appropriate columns +- [ ] Migration runs without errors on H2, MySQL, PostgreSQL +- [ ] Column names follow snake_case convention + +--- + +## Task 7: Testing + +### 7.1 Unit Tests + +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java` + +**Required Tests**: +1. Basic CRUD operations +2. Document field persistence +3. CustomerAccount field persistence +4. Embedded object persistence (ElectronicAddress, Organisation, Status) +5. AccountNotification collection persistence +6. Customer relationship +7. Validation constraints + +### 7.2 DTO Marshalling Tests + +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoMarshallingTest.java` (create new) + +**Required Tests**: +```java +@Test +@DisplayName("Should marshal CustomerAccount with all fields populated") +void shouldMarshalCustomerAccountWithAllFields() { + // Create CustomerAccountDto with: + // - All Document fields + // - All CustomerAccount fields + // - All embedded objects (ElectronicAddress, Organisation, Status) + // - notifications collection + + // Marshal to XML + // Verify field order matches customer.xsd +} + +@Test +@DisplayName("Should marshal CustomerAccount with minimal data") +void shouldMarshalCustomerAccountWithMinimalData() { + // Create with only required fields + // Verify optional fields are omitted (not null elements) +} + +@Test +@DisplayName("Should verify CustomerAccount field order matches customer.xsd") +void shouldVerifyCustomerAccountFieldOrder() { + // Create comprehensive CustomerAccountDto + // Marshal to XML + // Parse XML and verify element positions + // Document fields before CustomerAccount fields +} + +@Test +@DisplayName("Should use correct Customer namespace prefix (cust:)") +void shouldUseCorrectCustomerNamespace() { + // Verify xmlns:cust="http://naesb.org/espi/customer" + // Verify cust: prefix on all elements +} +``` + +### 7.3 Integration Tests + +**Files**: +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java` (create new) +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java` (create new) + +**Required Tests**: +1. Full CRUD operations with real MySQL/PostgreSQL databases +2. Document field persistence verification +3. CustomerAccount field persistence verification +4. Embedded objects persistence (ElectronicAddress, Organisation, Status) +5. AccountNotification collection persistence +6. Customer relationship persistence +7. Bulk operations (saveAll, deleteAll) + +### 7.4 Migration Verification Tests + +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java` + +**Add Tests**: +```java +@Test +@DisplayName("CustomerAccount entity with all embedded objects should work") +void customerAccountWithAllEmbeddedObjectsShouldWork() { + // Test CustomerAccount with: + // - All Document fields + // - ElectronicAddress embedded + // - Organisation embedded + // - Status embedded + // - AccountNotification collection +} + +@Test +@DisplayName("CustomerAccount embedded objects should be null-safe") +void customerAccountEmbeddedObjectsShouldBeNullSafe() { + // Test with null embedded objects + // Should not throw exceptions +} +``` + +### 7.5 XSD Validation Tests + +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/xsd/CustomerAccountXsdValidationTest.java` (create new) + +**Required Tests**: +```java +@Test +@DisplayName("Should validate marshalled CustomerAccount XML against customer.xsd") +void shouldValidateMarshalledXmlAgainstXsd() { + // Marshal CustomerAccountDto to XML + // Validate against customer.xsd using SchemaFactory + // Assert no validation errors +} +``` + +### Test Coverage Requirements + +- [ ] CustomerAccountRepositoryTest: 100% coverage of CRUD operations +- [ ] CustomerAccountDtoMarshallingTest: All field orders verified +- [ ] CustomerAccountMySQLIntegrationTest: Full CRUD with MySQL +- [ ] CustomerAccountPostgreSQLIntegrationTest: Full CRUD with PostgreSQL +- [ ] MigrationVerificationTest: CustomerAccount cases added +- [ ] XSD validation passes for all test cases +- [ ] All embedded object tests pass +- [ ] All collection tests pass +- [ ] All 609+ tests passing (existing + new) + +--- + +## Task 8: Commit, Push, PR + +### Pre-Commit Checklist + +- [ ] All 8 tasks completed +- [ ] All files modified are listed below +- [ ] Field order verified against customer.xsd +- [ ] All tests passing (mvn test) +- [ ] Integration tests passing with Docker (mvn verify -Pintegration-tests) +- [ ] No compilation warnings +- [ ] Code formatted consistently +- [ ] JavaDoc updated for new/modified fields + +### Files to Commit + +**Entity & Domain**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java` + +**DTO**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java` + +**Mapper**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java` + +**Repository**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepository.java` + +**Service**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAccountService.java` +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAccountServiceImpl.java` + +**Flyway Migration**: +- `openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Tests**: +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java` +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoMarshallingTest.java` (new) +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java` (new) +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java` (new) +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/xsd/CustomerAccountXsdValidationTest.java` (new) +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java` + +### Git Commands + +```bash +# Create feature branch +git checkout main +git pull origin main +git checkout -b feature/schema-compliance-phase-18-customer-account + +# Make all changes (Tasks 1-7) + +# Stage all changes +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepository.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAccountService.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAccountServiceImpl.java +git add openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoMarshallingTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/xsd/CustomerAccountXsdValidationTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java + +# Verify staged files +git status + +# Commit with detailed message +git commit -m "$(cat <<'EOF' +feat: ESPI 4.0 Schema Compliance - Phase 18: CustomerAccount + +Comprehensive schema compliance updates for CustomerAccount entity and all +associated components to match NAESB ESPI 4.0 customer.xsd specification. + +## Entity Changes (CustomerAccountEntity) +- Added missing Document base class fields: authorName, electronicAddress, docStatus +- Reordered Document fields to match XSD sequence +- Fixed contactInfo type from String to Organisation embedded object +- Reordered CustomerAccount fields to match XSD sequence +- Added proper @Embedded and @AttributeOverrides for all embedded objects + +## DTO Changes (CustomerAccountDto) +- Complete rewrite of @XmlType propOrder to match XSD element sequence +- Added all missing Document fields (9 fields) +- Fixed contactInfo type to Organisation +- Added notifications collection +- Removed non-XSD fields (accountNumber, transactionDate) +- Removed embedded relationship objects (Customer, CustomerAgreements) + +## Mapper Changes (CustomerAccountMapper) +- Added mappings for all Document fields +- Added mappings for embedded objects (ElectronicAddress, Organisation, Status) +- Added mapping for notifications collection +- Updated both toDto() and toEntity() methods + +## Repository Updates (CustomerAccountRepository) +- Verified only indexed field queries present +- Maintained JpaRepository standard methods + +## Service Updates +- Updated validation to handle new Document fields +- Updated business logic for embedded objects +- Verified field access order matches schema + +## Flyway Migration Updates (V3__Create_additiional_Base_Tables.sql) +- Added missing Document columns (authorName, electronicAddress_*, docStatus_*) +- Fixed contactInfo from String to Organisation embedded columns +- Verified column order matches XSD element sequence +- Added appropriate indexes + +## Testing +- Enhanced CustomerAccountRepositoryTest with embedded object tests +- Created CustomerAccountDtoMarshallingTest (3 tests) verifying field order +- Created CustomerAccountMySQLIntegrationTest (8 tests) +- Created CustomerAccountPostgreSQLIntegrationTest (8 tests) +- Created CustomerAccountXsdValidationTest for schema validation +- Enhanced MigrationVerificationTest with CustomerAccount cases +- All 650+ tests passing + +## Schema Compliance +- ✅ Document base class field order matches customer.xsd +- ✅ CustomerAccount field order matches customer.xsd +- ✅ All embedded objects properly structured +- ✅ All collections properly mapped +- ✅ XML output validates against customer.xsd +- ✅ Namespace prefix (cust:) correctly applied + +Related to Issue #28 Phase 18 + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" + +# Push to remote +git push origin feature/schema-compliance-phase-18-customer-account + +# Create PR using GitHub CLI +gh pr create \ + --title "feat: ESPI 4.0 Schema Compliance - Phase 18: CustomerAccount" \ + --body "$(cat <<'EOF' +## Summary +Comprehensive schema compliance updates for CustomerAccount entity to match NAESB ESPI 4.0 customer.xsd specification. + +## Changes +- **Entity**: Added missing Document fields, fixed field types and order +- **DTO**: Complete rewrite to match XSD element sequence +- **Mapper**: Added mappings for all new fields and embedded objects +- **Repository**: Verified query compliance +- **Service**: Updated validation and business logic +- **Migration**: Added missing columns, fixed contactInfo structure +- **Tests**: 5 new test files, enhanced existing tests + +## Schema Compliance +✅ All Document base class fields present and ordered correctly +✅ All CustomerAccount fields present and ordered correctly +✅ contactInfo properly embedded as Organisation +✅ All embedded objects structured correctly +✅ XML validates against customer.xsd + +## Test Results +- All 650+ tests passing +- Integration tests verified with MySQL 8.0 and PostgreSQL 18 +- XSD validation passes + +## Checklist +- [x] All 8 phase tasks completed +- [x] Field order matches customer.xsd exactly +- [x] All tests passing +- [x] Integration tests passing +- [x] XSD validation passing +- [x] No compilation warnings +- [x] Documentation updated + +Related to #28 +EOF +)" \ + --base main +``` + +--- + +## Success Criteria + +Phase 18 is complete when: + +1. ✅ CustomerAccountEntity has all Document + CustomerAccount fields in XSD order +2. ✅ CustomerAccountDto has all fields in XSD propOrder +3. ✅ contactInfo is Organisation type (not String) +4. ✅ All embedded objects properly structured (ElectronicAddress, Organisation, Status) +5. ✅ CustomerAccountMapper handles all fields correctly +6. ✅ Flyway migration adds all missing columns +7. ✅ All unit tests pass +8. ✅ All integration tests pass (MySQL, PostgreSQL) +9. ✅ XSD validation passes +10. ✅ PR created and ready for review + +--- + +## Dependencies + +**Referenced By**: +- CustomerAgreement (Phase 21 - will reference CustomerAccount via Atom links) +- Statement (Phase 19 - will reference CustomerAccount via Atom links) +- Customer (Phase 20 - completed, references CustomerAccount via Atom links) + +**This Phase Must Complete Before**: +- Phase 19: Statement (needs CustomerAccount relationship) +- Phase 21: CustomerAgreement (needs CustomerAccount relationship) + +--- + +## Estimated Effort + +- Task 1 (Entity): 2-3 hours +- Task 2 (DTO): 2-3 hours +- Task 3 (Mapper): 1-2 hours +- Task 4 (Repository): 30 minutes +- Task 5 (Service): 1 hour +- Task 6 (Migration): 1-2 hours +- Task 7 (Testing): 4-5 hours +- Task 8 (Commit/PR): 30 minutes + +**Total**: 12-17 hours + +--- + +## Notes + +- CustomerAccount extends Document (not directly IdentifiedObject) +- Document has 9 fields that must all be present in entity and DTO +- contactInfo is Organisation embedded object (complex nested structure) +- ElectronicAddress embedded in Document.electronicAddress +- Status embedded in Document.docStatus +- AccountNotification is a separate @ElementCollection +- isPrePay is an extension field (not in XSD) - must be at end with [extension] comment +- Customer relationship is for JPA only - not serialized to XML \ No newline at end of file diff --git a/PHASE_20_CUSTOMER_IMPLEMENTATION_PLAN.md b/PHASE_20_CUSTOMER_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..14578e8b --- /dev/null +++ b/PHASE_20_CUSTOMER_IMPLEMENTATION_PLAN.md @@ -0,0 +1,370 @@ +# Phase 20: Customer - ESPI 4.0 Schema Compliance Implementation Plan + +## Overview +Ensure CustomerEntity and all related components strictly comply with ESPI 4.0 customer.xsd schema definition. + +**Branch**: `feature/schema-compliance-phase-20-customer` + +**Dependencies**: +- TimeConfiguration (via bidirectional Atom rel='related' links) +- Statement (via bidirectional Atom rel='related' links) +- CustomerAccount (via bidirectional Atom rel='related' links) + +**Referenced By**: None + +--- + +## Current State Analysis + +### CustomerEntity.java - Current Structure +**Location**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java` + +**Current Field Order**: +1. ✅ organisation (embedded Organisation) - From OrganisationRole +2. ✅ kind (CustomerKind enum) +3. ✅ specialNeed (String) +4. ✅ vip (Boolean) +5. ✅ pucNumber (String) +6. ✅ status (embedded Status) +7. ✅ priority (embedded Priority) +8. ✅ locale (String) +9. ✅ customerName (String) +10. customerAccounts (OneToMany relationship) +11. timeConfiguration (OneToOne relationship) +12. statements (OneToMany relationship) +13. phoneNumbers (OneToMany relationship) + +### customer.xsd - Required Structure + +**Customer extends OrganisationRole extends IdentifiedObject** + +**XSD Element Sequence**: +1. IdentifiedObject fields (mRID, description) +2. **Organisation** (from OrganisationRole) +3. **kind** (CustomerKind enum) +4. **specialNeed** (String256) +5. **vip** (boolean) +6. **pucNumber** (String256) +7. **status** (Status) +8. **priority** (Priority) +9. **locale** (String256) +10. **customerName** (String256) + +**Status Structure** (embedded): +- value (String256) +- dateTime (DateTimeInterval) +- reason (String256) + +**Priority Structure** (embedded): +- value (Integer) +- rank (Integer) +- type (String256) + +**Organisation Structure** (embedded): +- organisationName (String256) +- streetAddress (StreetAddress) +- postalAddress (StreetAddress) +- electronicAddress (ElectronicAddress) + +### Compliance Assessment + +✅ **COMPLIANT**: Field order matches customer.xsd sequence +✅ **COMPLIANT**: All required embedded classes present +✅ **COMPLIANT**: Extends IdentifiedObject (correct per XSD) +✅ **COMPLIANT**: Relationships defined (CustomerAccount, TimeConfiguration, Statement) + +**Minor Issues to Address**: +1. ⚠️ Verify Organisation embedded field structure matches XSD exactly +2. ⚠️ Verify Status embedded class matches XSD (dateTime type) +3. ⚠️ Verify Priority embedded class matches XSD +4. ⚠️ Check Flyway migration column order matches XSD sequence +5. ⚠️ Review CustomerDto field order +6. ⚠️ Review CustomerMapper mappings +7. ⚠️ Review CustomerRepository for non-indexed queries +8. ⚠️ Add XML marshalling tests + +--- + +## Implementation Tasks + +### Task 1: Entity Updates (CustomerEntity.java) +**Status**: ✅ Mostly Complete - Verify Only + +**Actions**: +1. ✅ Verify field order matches customer.xsd sequence (appears correct) +2. ⚠️ Verify Organisation embedded class structure: + - Check streetAddress and postalAddress field mapping + - Check electronicAddress field mapping + - Verify column name prefixes are consistent +3. ⚠️ Verify Status embedded class: + - Confirm dateTime uses OffsetDateTime (correct type) + - Verify column names +4. ⚠️ Verify Priority embedded class: + - Confirm all three fields present (value, rank, type) + - Verify column names +5. ✅ Relationships look correct (CustomerAccount, TimeConfiguration, Statement) +6. ⚠️ Check phoneNumbers relationship - ensure it's handled correctly + +**Files to Review**: +- `CustomerEntity.java` +- `Organisation.java` (if separate embeddable) +- `Status.java` (inner class) +- `Priority.java` (inner class) + +--- + +### Task 2: DTO Updates (CustomerDto.java) +**Status**: ⚠️ Needs Review + +**Actions**: +1. Read CustomerDto and verify field order matches customer.xsd +2. Ensure Organisation DTO structure matches XSD +3. Ensure Status DTO structure matches XSD +4. Ensure Priority DTO structure matches XSD +5. Verify JAXB annotations for XML marshalling +6. Ensure namespace is "http://naesb.org/espi/customer" + +**Files to Review**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java` + +--- + +### Task 3: MapStruct Mapper Updates (CustomerMapper.java) +**Status**: ⚠️ Needs Review + +**Actions**: +1. Review CustomerMapper interface +2. Verify Entity-to-DTO conversion mappings +3. Verify DTO-to-Entity conversion mappings +4. Ensure embedded Organisation mapping is correct +5. Ensure embedded Status mapping is correct +6. Ensure embedded Priority mapping is correct +7. Handle relationship mappings (ignore or separate methods) +8. Remove any IdentifiedObject field mappings (handled by base) + +**Files to Review**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java` + +--- + +### Task 4: Repository Updates (CustomerRepository.java) +**Status**: ⚠️ Needs Review + +**Actions**: +1. Review CustomerRepository interface +2. Keep ONLY queries on indexed fields: + - id (primary key) + - created, updated (likely indexed) + - kind (likely indexed) + - Any other explicitly indexed fields +3. Remove queries on non-indexed fields +4. Review test requirements and ensure indexed queries support them + +**Files to Review**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepository.java` + +--- + +### Task 5: Service Updates +**Status**: ⚠️ Needs Review + +**Actions**: +1. Review CustomerService interface +2. Review CustomerServiceImpl implementation +3. Verify service methods support schema-compliant operations +4. Ensure proper relationship handling (CustomerAccount, TimeConfiguration, Statement) +5. Check for any legacy patterns that need updating + +**Files to Review**: +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerService.java` +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerServiceImpl.java` (if exists) + +--- + +### Task 6: Flyway Migration Updates +**Status**: ⚠️ Needs Investigation + +**Actions**: +1. Locate Customer table creation in Flyway migrations +2. Verify column order matches customer.xsd element sequence +3. Check Organisation embedded fields have correct column names +4. Check Status embedded fields have correct column names +5. Check Priority embedded fields have correct column names +6. Verify foreign key relationships (time_configuration_id) +7. Verify indexes on commonly queried fields + +**Files to Locate**: +- `openespi-common/src/main/resources/db/migration/V*.sql` (Customer table) +- `openespi-common/src/main/resources/db/vendor/*/V*.sql` (vendor-specific) + +--- + +### Task 7: Testing +**Status**: ⚠️ Needs Work + +**Actions**: +1. **Unit Tests**: + - Review CustomerRepositoryTest + - Add tests for all indexed query methods + - Test embedded Organisation fields + - Test embedded Status fields + - Test embedded Priority fields + - Test relationship loading (CustomerAccount, TimeConfiguration, Statement) + +2. **Integration Tests**: + - Add TestContainers-based integration tests + - Test full CRUD operations + - Test relationship persistence + +3. **XML Marshalling Tests**: + - Add XML marshalling test for CustomerEntity → CustomerDto → XML + - Add XML unmarshalling test for XML → CustomerDto → CustomerEntity + - Validate generated XML against customer.xsd schema + - Test embedded Organisation serialization + - Test embedded Status serialization + - Test embedded Priority serialization + - Verify namespace is "http://naesb.org/espi/customer" + +4. **Migration Tests**: + - Use MigrationVerificationTest pattern + - Verify Customer table structure matches XSD + +**Files to Review/Create**: +- `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepositoryTest.java` +- Create: `CustomerIntegrationTest.java` +- Create: `CustomerXmlMarshallingTest.java` + +--- + +### Task 8: Commit, Push, PR + +**Actions**: +1. Create feature branch: `feature/schema-compliance-phase-20-customer` +2. Stage all changes +3. Commit with message: + ``` + feat: Phase 20 - Customer ESPI 4.0 Schema Compliance + + Ensured CustomerEntity and related components comply with customer.xsd. + + Changes: + - Verified CustomerEntity field order matches customer.xsd + - Updated CustomerDto field order and JAXB annotations + - Updated CustomerMapper mappings + - Cleaned up CustomerRepository (removed non-indexed queries) + - Verified/updated Flyway migrations + - Added XML marshalling tests + - Updated unit and integration tests + + Customer is now 100% ESPI 4.0 customer.xsd compliant. + + Co-Authored-By: Claude Sonnet 4.5 + ``` +4. Push branch to remote +5. Create PR with comprehensive description +6. Wait for CI/CD checks to pass +7. Request review + +--- + +## Estimated Sub-Phases + +Based on complexity, Phase 20 can be broken into: + +### Phase 20a: Entity and DTO Verification (Low Risk) +- Review and verify CustomerEntity field order +- Review and verify embedded classes (Organisation, Status, Priority) +- Review and verify CustomerDto structure +- Update JAXB annotations if needed + +### Phase 20b: Mapper and Repository Cleanup (Medium Risk) +- Review and update CustomerMapper +- Clean up CustomerRepository (remove non-indexed queries) +- Update service layer if needed + +### Phase 20c: Flyway Migration Review (Medium Risk) +- Locate and review Customer table migrations +- Verify column order matches XSD +- Add migration script if changes needed + +### Phase 20d: Testing (Low Risk) +- Add/update unit tests +- Add integration tests +- Add XML marshalling/unmarshalling tests +- Verify schema validation + +--- + +## Success Criteria + +✅ CustomerEntity field order matches customer.xsd sequence exactly +✅ CustomerDto field order matches customer.xsd sequence exactly +✅ All embedded classes (Organisation, Status, Priority) match XSD +✅ CustomerMapper correctly maps all fields +✅ CustomerRepository contains only indexed field queries +✅ Flyway migration column order matches XSD +✅ All unit tests pass +✅ Integration tests pass with TestContainers +✅ XML marshalling tests validate against customer.xsd +✅ CI/CD pipeline passes all checks +✅ PR approved and merged + +--- + +## Risk Assessment + +**Low Risk**: +- Entity structure appears correct +- DTO and Mapper infrastructure exists +- Service layer exists + +**Medium Risk**: +- Flyway migrations may need column reordering +- Repository may have non-indexed queries to remove +- XML marshalling tests need to be created + +**High Risk**: +- None identified + +--- + +## Dependencies and Blockers + +**Dependencies**: +- TimeConfiguration must be schema-compliant (already done in earlier phases) +- Statement entity must exist (verify) +- CustomerAccount entity must exist (verify) + +**Blockers**: +- None identified + +--- + +## Next Steps After Phase 20 + +After completing Phase 20 (Customer), proceed to: +- **Phase 21: ServiceSupplier** +- **Phase 22: Asset** +- **Phase 23: ServiceLocation** +- **Phase 24: CustomerAgreement** + +These phases will complete the customer.xsd schema compliance work. + +--- + +## Notes + +1. Customer is a PII (Personally Identifiable Information) entity in the customer.xsd namespace +2. Customer extends OrganisationRole which extends IdentifiedObject +3. Customer has multiple embedded complex types (Organisation, Status, Priority) +4. Customer has relationships to TimeConfiguration, Statement, and CustomerAccount +5. The entity appears well-structured and mostly compliant already +6. Main work will be verification, testing, and documentation + +--- + +**Created**: 2026-01-16 +**Phase**: 20 +**Entity**: Customer +**Schema**: customer.xsd +**Status**: Ready for Implementation diff --git a/PHASE_23_EXPANDED_SCOPE.md b/PHASE_23_EXPANDED_SCOPE.md new file mode 100644 index 00000000..35e18416 --- /dev/null +++ b/PHASE_23_EXPANDED_SCOPE.md @@ -0,0 +1,161 @@ +# Phase 23 Expanded Scope: Full Embedded Type Compliance + +## Overview +Phase 23 expanded to include full customer.xsd compliance for ALL embedded types used by ServiceLocation and other customer entities. + +## Compliance Gaps Identified + +### 1. PhoneNumberEntity (TelephoneNumber) - Missing 4 Fields +**Current:** 4 fields (areaCode, cityCode, localNumber, extension) +**XSD Required:** 8 fields (customer.xsd lines 1428-1478) + +**Missing Fields:** +- `countryCode` (String256) +- `dialOut` (String256) +- `internationalPrefix` (String256) +- `ituPhone` (String256) + +### 2. Organisation.StreetAddress - Simplified Structure +**Current:** 5 simple string fields +**XSD Required:** Complex nested structure with 23+ fields + +**Current Implementation:** +```java +public static class StreetAddress { + private String streetDetail; // Should be StreetDetail (12 fields) + private String townDetail; // Should be TownDetail (6 fields) + private String stateOrProvince; // Should be in TownDetail + private String postalCode; // Correct + private String country; // Should be in TownDetail +} +``` + +**XSD Structure (customer.xsd lines 1285-1320):** +```xml + + + + + + + +``` + +**Required Nested Types:** + +#### StreetDetail (12 fields, customer.xsd lines 1321-1391): +- number +- name +- suffix +- prefix +- type +- code +- buildingName +- suiteNumber +- addressGeneral +- addressGeneral2 +- addressGeneral3 +- withinTownLimits + +#### TownDetail (6 fields, customer.xsd lines 1478-1519): +- code +- section +- name +- county +- stateOrProvince +- country + +### 3. Status - Missing remark Field +**Current:** 3 fields (value, dateTime, reason) +**XSD Required:** 4 fields (value, dateTime, remark, reason) + +**Status:** Already being fixed in Phase 23 for ServiceLocationEntity + +## Implementation Strategy + +### Option A: Pragmatic Approach (Recommended) +Keep simplified StreetAddress structure for now, add TODO comments, plan separate phase for full compliance later. + +**Rationale:** +- Full StreetAddress compliance affects 5+ entities (Customer, CustomerAccount, CustomerAgreement, ServiceLocation, ServiceSupplier) +- Would require significant migration script changes +- Phase 23 scope already large +- Can defer to dedicated StreetAddress/TownDetail/StreetDetail compliance phase + +**Phase 23 Actions:** +1. ✅ Fix PhoneNumberEntity (add 4 missing fields) +2. ✅ Fix Status remark field +3. ✅ Add usagePointHrefs collection +4. ✅ Complete ServiceLocationDto refactoring +5. ⚠️ Keep simplified StreetAddress with TODO +6. ✅ Create ServiceLocationMapper +7. ✅ Update tests and migration + +### Option B: Full Compliance Approach +Implement complete StreetAddress/StreetDetail/TownDetail structure now. + +**Rationale:** +- Achieves full XSD compliance immediately +- Avoids technical debt +- Eliminates need for future refactoring phase + +**Phase 23 Actions:** +1. Create StreetDetail embeddable (12 fields) +2. Create TownDetail embeddable (6 fields) +3. Update Organisation.StreetAddress to use nested embeddables +4. Fix PhoneNumberEntity (add 4 missing fields) +5. Fix Status remark field +6. Add usagePointHrefs collection +7. Update all entity @AttributeOverride annotations (5+ entities) +8. Complete ServiceLocationDto refactoring +9. Update all DTOs for StreetAddress changes +10. Create ServiceLocationMapper +11. Update migration scripts (significant changes) +12. Update all tests + +## Decision: Option A (Pragmatic) + +Proceeding with **Option A** to keep Phase 23 manageable while still making significant progress. + +**Deferred to Future Phase:** +- Full StreetAddress/StreetDetail/TownDetail compliance +- Will create separate "Phase XX: StreetAddress Schema Compliance" issue + +## Phase 23 Expanded Task List + +### Completed +- ✅ T1: Create Feature Branch +- ✅ T2: Update ServiceLocationEntity with Status Remark Field +- ✅ T3: Add usagePointHrefs Collection to ServiceLocationEntity +- ✅ T4: Verify ServiceLocationEntity Field Order +- ✅ T5: Refactor ServiceLocationDto - Remove Atom Fields +- ✅ T6: Refactor ServiceLocationDto - Remove Relationship Fields + +### In Progress +- 🔄 T7: Add PhoneNumberEntity Missing Fields (NEW) +- 🔄 T8: Update PhoneNumberEntity Migration (NEW) +- 🔄 T9: Add Location Fields to ServiceLocationDto +- 🔄 T10: Add ServiceLocation Fields to DTO +- 🔄 T11: Update ServiceLocationDto propOrder + +### Pending +- ⏳ T12: Create ServiceLocationMapper Interface +- ⏳ T13: Review and Clean Up ServiceLocationRepository +- ⏳ T14: Update Flyway Migration Script +- ⏳ T15: Create ServiceLocationDtoTest +- ⏳ T16: Update ServiceLocationRepositoryTest +- ⏳ T17: Run All Tests and Fix Failures +- ⏳ T18: Run Integration Tests +- ⏳ T19: Commit and Push Changes +- ⏳ T20: Create Pull Request +- ⏳ T21: Update Issue #28 + +## Next Steps + +1. Add 4 missing fields to PhoneNumberEntity +2. Update phone_numbers migration table +3. Update CustomerDto.TelephoneNumberDto to include all 8 fields +4. Continue with ServiceLocationDto refactoring +5. Complete Phase 23 with full PhoneNumber compliance +6. Add TODO comments for StreetAddress full compliance +7. Create follow-up issue for StreetAddress/StreetDetail/TownDetail compliance diff --git a/PHASE_23_PROGRESS_CHECKPOINT.md b/PHASE_23_PROGRESS_CHECKPOINT.md new file mode 100644 index 00000000..676424cd --- /dev/null +++ b/PHASE_23_PROGRESS_CHECKPOINT.md @@ -0,0 +1,249 @@ +# Phase 23 Progress Checkpoint + +## Completed Work (T1-T9) + +### Entity Layer - ✅ COMPLETE +1. ✅ **ServiceLocationEntity.java** + - Added `status.remark` @AttributeOverride (4th field) + - Added `usagePointHrefs` List with @ElementCollection + - Replaced `phoneNumbers` collection with embedded `phone1` and `phone2` + - Used repeatable @AttributeOverride (JDK 25 feature) + - All 16 phone fields mapped (8 per phone × 2 phones) + +2. ✅ **Organisation.TelephoneNumber** @Embeddable Created + - 8 fields per customer.xsd lines 1428-1478 + - countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone + - Lombok @Getter/@Setter with manual equals/hashCode/toString + +3. ✅ **CustomerDto.TelephoneNumberDto** Created + - 8 fields with full JAXB annotations + - @XmlType with propOrder + - Implements Serializable + - Renamed from PhoneNumberDto → TelephoneNumberDto + +### DTO Layer - ⚠️ IN PROGRESS +4. ✅ **ServiceLocationDto.java** - Partial + - ✅ Removed 5 Atom fields (published, updated, selfLink, upLink, relatedLinks) + - ✅ Removed description field (Atom title) + - ✅ Removed customerAgreement relationship field + - ✅ Removed helper methods (getSelfHref, generateSelfHref, etc.) + - ✅ Updated imports (removed LinkDto, OffsetDateTime) + - ❌ Missing Location fields (type, mainAddress, secondaryAddress, phone1, phone2, electronicAddress, status) + - ❌ Missing usagePointHrefs collection field + - ❌ Missing outageBlock field + - ❌ PropOrder not updated + +## Remaining Work (T10-T23) + +### T10: Add Location Fields to ServiceLocationDto +**Fields to Add:** +```java +// Location fields (from IdentifiedObject + Location) +@XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") +private String type; + +@XmlElement(name = "mainAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.StreetAddressDto mainAddress; + +@XmlElement(name = "secondaryAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.StreetAddressDto secondaryAddress; + +@XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") +private CustomerDto.TelephoneNumberDto phone1; + +@XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") +private CustomerDto.TelephoneNumberDto phone2; + +@XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.ElectronicAddressDto electronicAddress; + +@XmlElement(name = "status", namespace = "http://naesb.org/espi/customer") +private StatusDto status; + +// StatusDto nested class (4 fields) +public static class StatusDto implements Serializable { + // value, dateTime, remark, reason +} +``` + +### T11: Add ServiceLocation Fields to DTO +**Fields to Add:** +```java +@XmlElement(name = "UsagePoints", namespace = "http://naesb.org/espi/customer") +@XmlElementWrapper(name = "UsagePoints", namespace = "http://naesb.org/espi/customer") +private List usagePointHrefs; + +@XmlElement(name = "outageBlock", namespace = "http://naesb.org/espi/customer") +private String outageBlock; +``` + +### T12: Update ServiceLocationDto propOrder +**XSD Field Sequence:** +```java +@XmlType(name = "ServiceLocation", namespace = "http://naesb.org/espi/customer", propOrder = { + // Location fields + "type", "mainAddress", "secondaryAddress", "phone1", "phone2", + "electronicAddress", "geoInfoReference", "direction", "status", + // ServiceLocation fields + "accessMethod", "siteAccessProblem", "needsInspection", "usagePointHrefs", "outageBlock" +}) +``` + +### T13: Create ServiceLocationMapper +**Mapper Interface:** +- Bidirectional Entity ↔ DTO mappings +- phone1/phone2 embedded mapping +- usagePointHrefs collection mapping +- All Location field mappings + +### T14: Clean Up ServiceLocationRepository +**Remove non-index queries:** +- findLocationsThatNeedInspection +- findLocationsWithAccessProblems +- findByMainAddressStreetContaining +- findByDirectionContaining +- findByPhone1AreaCode + +**Keep only:** +- JpaRepository inherited methods +- Index-based queries if needed for tests + +### T15: Update Flyway Migration +**Add to service_locations table:** +```sql +-- Status remark +status_remark VARCHAR(256), + +-- Phone1 fields (8) +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), + +-- Phone2 fields (8) +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) +``` + +**Create new table:** +```sql +CREATE TABLE service_location_usage_point_hrefs ( + service_location_id CHAR(36) NOT NULL, + usage_point_href VARCHAR(512), + FOREIGN KEY (service_location_id) REFERENCES service_locations(id) +); +``` + +**Drop old table:** +```sql +-- Remove phone_numbers table (replaced by embedded fields) +DROP TABLE IF EXISTS phone_numbers; +``` + +### T16: Update Other Entities +**Entities needing phone1/phone2 embedded:** +- CustomerEntity +- CustomerAccountEntity +- ServiceSupplierEntity +- MeterEntity (extends EndDeviceEntity) + +**For each entity:** +1. Replace phoneNumbers collection with phone1/phone2 embedded +2. Add 16 @AttributeOverride annotations (8 per phone) +3. Update toString() method + +### T17-T20: Testing +- Create ServiceLocationDtoTest (5+ tests) +- Update ServiceLocationRepositoryTest +- Run all tests (fix failures) +- Run integration tests + +### T21-T23: Finalization +- Commit and push +- Create PR +- Update Issue #28 + +## Key Decisions Made + +### 1. @Embeddable vs Separate Table for TelephoneNumber +**Decision:** Use @Embeddable +**Rationale:** +- Performance: 1 query instead of 2, no JOIN required +- XSD alignment: phone1/phone2 are elements, not collection +- Fixed number: Always 0-2 phones, not unbounded + +### 2. UsagePointHrefs Collection vs Single String +**Decision:** Use List with @ElementCollection +**Rationale:** +- customer.xsd defines UsagePoints with maxOccurs="unbounded" +- Multiple usage points can serve one service location +- Each string stores atom:link[@rel='self']/@href URL + +### 3. Repeatable @AttributeOverride vs @AttributeOverrides Wrapper +**Decision:** Use repeatable @AttributeOverride directly +**Rationale:** +- JDK 25 supports repeatable annotations +- Cleaner, more readable code +- No wrapper needed + +### 4. StreetAddress Compliance +**Decision:** Defer full StreetAddress/StreetDetail/TownDetail compliance +**Rationale:** +- Affects 5+ entities (large scope) +- Phase 23 already expanded significantly +- Can be separate future phase +- Current simplified structure functional + +## Session Management + +**Current Token Usage:** ~130K / 200K +**Remaining Work:** ~70K tokens needed +**Recommendation:** Complete T10-T12 (DTO refactoring), then checkpoint for continuation + +## Next Immediate Steps + +1. Add Location fields to ServiceLocationDto (T10) +2. Add ServiceLocation fields (usagePointHrefs, outageBlock) to DTO (T11) +3. Update propOrder (T12) +4. Create ServiceLocationMapper (T13) + +**Estimated Tokens:** +- T10-T12: ~15K tokens +- T13: ~10K tokens +- T14-T15: ~10K tokens +- Tests: ~20K tokens +- **Total**: ~55K tokens remaining (fits in current session) + +## Files Modified So Far + +1. ServiceLocationEntity.java ✅ +2. Organisation.java (added TelephoneNumber) ✅ +3. CustomerDto.java (added TelephoneNumberDto) ✅ +4. ServiceLocationDto.java (partial) ⚠️ + +## Files to Modify + +5. ServiceLocationDto.java (complete Location fields) +6. ServiceLocationMapper.java (create new) +7. ServiceLocationRepository.java (clean up) +8. V3__Create_additiional_Base_Tables.sql (add columns, tables) +9. CustomerEntity.java (phone1/phone2) +10. CustomerAccountEntity.java (phone1/phone2) +11. ServiceSupplierEntity.java (phone1/phone2) +12. MeterEntity.java (phone1/phone2) +13. ServiceLocationDtoTest.java (create new) +14. ServiceLocationRepositoryTest.java (update) + +**Total Files:** 14 files to modify/create +**Files Complete:** 3/14 (21%) +**Files Remaining:** 11/14 (79%) diff --git a/PHASE_23_SERVICELOCATION_IMPLEMENTATION_PLAN.md b/PHASE_23_SERVICELOCATION_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..bbfd8623 --- /dev/null +++ b/PHASE_23_SERVICELOCATION_IMPLEMENTATION_PLAN.md @@ -0,0 +1,541 @@ +# Phase 23: ServiceLocation - ESPI 4.0 Schema Compliance Implementation Plan + +## Overview +Implement complete ESPI 4.0 customer.xsd schema compliance for ServiceLocation entity, DTO, repository, service, and mapper layers. + +## Issue Reference +Issue #28 - Phase 23: ServiceLocation + +## XSD Schema Reference +**customer.xsd Inheritance Chain:** +- **ServiceLocation** (lines 1074-1116) extends **WorkLocation** +- **WorkLocation** (lines 1397-1402) extends **Location** +- **Location** (lines 914-997) extends **IdentifiedObject** + +**Total Fields:** 14 fields from Location + 5 fields from ServiceLocation = 19 fields + +## Current State Analysis + +### ServiceLocationEntity.java ✅ (Mostly Complete) +**Status:** Entity structure is mostly correct +- ✅ Extends IdentifiedObject +- ✅ Has all Location fields (type, mainAddress, secondaryAddress, phoneNumbers, electronicAddress, geoInfoReference, direction, status) +- ✅ Has all ServiceLocation fields (accessMethod, siteAccessProblem, needsInspection, outageBlock) +- ⚠️ Missing `status.remark` field (Status should have 4 fields: value, dateTime, remark, reason) +- ❌ Missing `usagePointHref` String field for cross-stream UsagePoint reference +- ❌ Field order may not match XSD sequence +- ❌ Phone numbers use phone1/phone2 naming in XSD, but entity uses phoneNumbers collection + +### ServiceLocationDto.java ❌ (Needs Major Refactoring) +**Status:** DTO has incorrect structure (still has Atom fields, missing Location fields) +- ❌ Has Atom protocol fields (published, updated, selfLink, upLink, relatedLinks) - should be REMOVED +- ❌ Has `positionAddress` String field - should be replaced with Location structure +- ❌ Has `customerAgreement` relationship field - should NOT be in DTO +- ❌ Missing Location fields: type, mainAddress, secondaryAddress, phone1, phone2, electronicAddress, status +- ✅ Has ServiceLocation fields: accessMethod, needsInspection, siteAccessProblem +- ❌ Missing outageBlock field +- ❌ Missing usagePointHref field +- ❌ Field order doesn't match XSD sequence + +### ServiceLocationMapper.java ❌ (Does Not Exist) +**Status:** Mapper needs to be created from scratch +- ❌ No mapper file exists +- Needs bidirectional Entity ↔ DTO mappings +- Needs to handle Location embedded types (StreetAddress, ElectronicAddress, Status) +- Needs to handle phone number collection mapping + +### ServiceLocationRepository.java ⚠️ (Needs Review) +**Status:** Repository has many non-index queries +- ✅ Extends JpaRepository +- ⚠️ Has 8 query methods (should review and keep only index-based queries) +- Current queries: findByOutageBlock, findLocationsThatNeedInspection, findLocationsWithAccessProblems, findByMainAddressStreetContaining, findByDirectionContaining, findByType, findByPhone1AreaCode, findByGeoInfoReference +- **Decision needed:** Which queries are truly index-based and required for tests? + +### ServiceLocationService.java ⚠️ (Needs Review) +**Status:** Service interface exists, needs schema compliance review + +### ServiceLocationServiceImpl.java ⚠️ (Needs Review) +**Status:** Service implementation exists, needs schema compliance review + +### Tests +- ✅ ServiceLocationRepositoryTest.java exists +- ❌ ServiceLocationDtoTest.java does NOT exist (needs to be created) +- ❌ ServiceLocationMapperTest.java may be needed + +### Flyway Migration +- ✅ V3__Create_additiional_Base_Tables.sql has service_locations table +- ⚠️ Needs review for column order and missing columns (status_remark, usage_point_href) + +## XSD Field Mapping + +### Location Fields (from IdentifiedObject + Location) +Per customer.xsd lines 914-997: + +| XSD Field | Entity Field | DTO Field | Type | Notes | +|-----------|--------------|-----------|------|-------| +| mRID | id (UUID) | uuid | String | IdentifiedObject.mRID | +| description | description | description | String | IdentifiedObject | +| type | type | type | String256 | Location classification | +| mainAddress | mainAddress | mainAddress | StreetAddress | Embedded | +| secondaryAddress | secondaryAddress | secondaryAddress | StreetAddress | Embedded | +| phone1 | phoneNumbers[0] | phone1 | TelephoneNumber | Collection mapping | +| phone2 | phoneNumbers[1] | phone2 | TelephoneNumber | Collection mapping | +| electronicAddress | electronicAddress | electronicAddress | ElectronicAddress | Embedded (8 fields) | +| geoInfoReference | geoInfoReference | geoInfoReference | String256 | | +| direction | direction | direction | String256 | | +| status | status | status | Status | Embedded (4 fields: value, dateTime, remark, reason) | +| positionPoints | - | - | PositionPoint[] | NOT IMPLEMENTED (complex geospatial) | + +### WorkLocation Fields +Per customer.xsd lines 1397-1402: +- WorkLocation adds NO fields (just extends Location) + +### ServiceLocation Fields +Per customer.xsd lines 1074-1116: + +| XSD Field | Entity Field | DTO Field | Type | Notes | +|-----------|--------------|-----------|------|-------| +| accessMethod | accessMethod | accessMethod | String256 | | +| siteAccessProblem | siteAccessProblem | siteAccessProblem | String256 | | +| needsInspection | needsInspection | needsInspection | Boolean | | +| UsagePoints | usagePointHref | usagePointHref | String | Cross-stream reference (NOT Atom link) | +| outageBlock | outageBlock | outageBlock | String32 | Extension field | + +## Critical Phase 23 Requirements + +### 1. Cross-Stream UsagePoint Reference +**Issue #28 Phase 23 Note:** +> "ServiceLocation exists in customer.xsd PII data stream, UsagePoint exists in espi.xsd non-PII data stream. ServiceLocation stores UsagePoint's rel="self" href URL directly, NOT via Atom link element." + +**Implementation:** +- Add `usagePointHref` String field to ServiceLocationEntity +- Add `usagePointHref` String field to ServiceLocationDto +- Store href URL string like: `"https://api.example.com/espi/1_1/resource/UsagePoint/550e8400-e29b-41d4-a716-446655440000"` +- **DO NOT** use Atom LinkDto for this reference + +### 2. Status 4-Field Compliance +Per Phase 24 findings, Status embedded type must have 4 fields: +- value (String) +- dateTime (Long - epoch seconds) +- remark (String) - **Currently missing in entity Status @AttributeOverride** +- reason (String) + +### 3. ElectronicAddress 8-Field Compliance +Per Phase 24 findings, ElectronicAddress must have all 8 fields: +- lan, mac, email1, email2, web, radio, userID, password +- Entity already has correct @AttributeOverride annotations + +### 4. Phone Number Mapping +XSD defines `phone1` and `phone2` as TelephoneNumber elements. +Entity uses `phoneNumbers` collection with PhoneNumberEntity. +Mapper needs to handle: +- Entity collection → DTO phone1/phone2 fields +- DTO phone1/phone2 → Entity collection + +### 5. No Atom Fields in DTO +Per Phase 20 customer.xsd compliance pattern: +- Remove: published, updated, selfLink, upLink, relatedLinks +- Only include XSD-defined fields +- DTO should be pure customer.xsd representation + +## Implementation Tasks + +### Task 1: Update ServiceLocationEntity.java +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java` + +**Changes:** +1. Add `status.remark` to @AttributeOverride annotations: +```java +@AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "status_value")), + @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")), + @AttributeOverride(name = "remark", column = @Column(name = "status_remark")), + @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) +}) +private Status status; +``` + +2. Add `usagePointHref` String field after `needsInspection`: +```java +/** + * Reference to UsagePoint resource href URL (cross-stream reference from customer.xsd to usage.xsd). + * Stores the full href URL, NOT an Atom link element. + * Example: "https://api.example.com/espi/1_1/resource/UsagePoint/550e8400-e29b-41d4-a716-446655440000" + */ +@Column(name = "usage_point_href", length = 512) +private String usagePointHref; +``` + +3. Verify field order matches XSD sequence: + - Location fields: type, mainAddress, secondaryAddress, phoneNumbers, electronicAddress, geoInfoReference, direction, status + - ServiceLocation fields: accessMethod, siteAccessProblem, needsInspection, usagePointHref, outageBlock + +4. Update equals/hashCode if needed (should use UUID-based pattern from Phase 24) + +### Task 2: Complete DTO Refactoring +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` + +**Changes:** +1. **REMOVE all Atom protocol fields:** + - Remove: `published`, `updated`, `selfLink`, `upLink`, `relatedLinks` + - Remove: `getSelfHref()`, `getUpHref()`, `generateSelfHref()`, `generateUpHref()` methods + +2. **REMOVE relationship fields:** + - Remove: `customerAgreement` field + +3. **ADD Location fields:** +```java +@XmlElement(name = "type") +private String type; + +@XmlElement(name = "mainAddress") +private CustomerDto.StreetAddressDto mainAddress; + +@XmlElement(name = "secondaryAddress") +private CustomerDto.StreetAddressDto secondaryAddress; + +@XmlElement(name = "phone1") +private CustomerDto.TelephoneNumberDto phone1; + +@XmlElement(name = "phone2") +private CustomerDto.TelephoneNumberDto phone2; + +@XmlElement(name = "electronicAddress") +private CustomerDto.ElectronicAddressDto electronicAddress; + +@XmlElement(name = "geoInfoReference") +private String geoInfoReference; + +@XmlElement(name = "direction") +private String direction; + +@XmlElement(name = "status") +private StatusDto status; +``` + +4. **ADD ServiceLocation fields:** +```java +@XmlElement(name = "UsagePoints") +private String usagePointHref; + +@XmlElement(name = "outageBlock") +private String outageBlock; +``` + +5. **REPLACE positionAddress with proper Location structure** + +6. **Update @XmlType propOrder** to match XSD sequence: +```java +@XmlType(name = "ServiceLocation", namespace = "http://naesb.org/espi/customer", propOrder = { + // Location fields + "type", "mainAddress", "secondaryAddress", "phone1", "phone2", + "electronicAddress", "geoInfoReference", "direction", "status", + // ServiceLocation fields + "accessMethod", "siteAccessProblem", "needsInspection", "usagePointHref", "outageBlock" +}) +``` + +7. **Create StatusDto nested class:** +```java +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "LocationStatus", namespace = "http://naesb.org/espi/customer", propOrder = { + "value", "dateTime", "remark", "reason" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public static class StatusDto implements Serializable { + @XmlElement(name = "value") + private String value; + + @XmlElement(name = "dateTime") + private Long dateTime; + + @XmlElement(name = "remark") + private String remark; + + @XmlElement(name = "reason") + private String reason; +} +``` + +### Task 3: Create ServiceLocationMapper +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java` + +**Implementation:** +```java +@Mapper(componentModel = "spring", uses = {CustomerMapper.class}) +public interface ServiceLocationMapper { + + // Entity to DTO + @Mapping(target = "uuid", source = "id") + @Mapping(target = "type", source = "type") + @Mapping(target = "mainAddress", source = "mainAddress") + @Mapping(target = "secondaryAddress", source = "secondaryAddress") + @Mapping(target = "phone1", expression = "java(mapPhone1(entity.getPhoneNumbers()))") + @Mapping(target = "phone2", expression = "java(mapPhone2(entity.getPhoneNumbers()))") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "geoInfoReference", source = "geoInfoReference") + @Mapping(target = "direction", source = "direction") + @Mapping(target = "status", source = "status") + @Mapping(target = "accessMethod", source = "accessMethod") + @Mapping(target = "siteAccessProblem", source = "siteAccessProblem") + @Mapping(target = "needsInspection", source = "needsInspection") + @Mapping(target = "usagePointHref", source = "usagePointHref") + @Mapping(target = "outageBlock", source = "outageBlock") + ServiceLocationDto toDto(ServiceLocationEntity entity); + + // DTO to Entity + @Mapping(target = "id", source = "uuid") + @Mapping(target = "phoneNumbers", expression = "java(mapPhoneNumbers(dto.getPhone1(), dto.getPhone2()))") + // ... all other mappings + ServiceLocationEntity toEntity(ServiceLocationDto dto); + + // Custom phone number mappings + default CustomerDto.TelephoneNumberDto mapPhone1(List phoneNumbers) { + if (phoneNumbers == null || phoneNumbers.isEmpty()) return null; + return mapTelephoneNumber(phoneNumbers.get(0)); + } + + default CustomerDto.TelephoneNumberDto mapPhone2(List phoneNumbers) { + if (phoneNumbers == null || phoneNumbers.size() < 2) return null; + return mapTelephoneNumber(phoneNumbers.get(1)); + } + + default List mapPhoneNumbers( + CustomerDto.TelephoneNumberDto phone1, + CustomerDto.TelephoneNumberDto phone2) { + List phoneNumbers = new ArrayList<>(); + if (phone1 != null) phoneNumbers.add(mapPhoneNumberEntity(phone1, "ServiceLocationEntity")); + if (phone2 != null) phoneNumbers.add(mapPhoneNumberEntity(phone2, "ServiceLocationEntity")); + return phoneNumbers; + } + + // Delegate to CustomerMapper for address/phone/electronicAddress mappings +} +``` + +### Task 4: Review ServiceLocationRepository +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java` + +**Changes:** +Per Issue #28 Phase 23: "Keep ONLY index field queries. Remove all non-index queries not required for tests." + +**Review each query:** +1. `findByOutageBlock` - Keep if outageBlock is indexed +2. `findLocationsThatNeedInspection` - Remove (needsInspection is boolean, not indexed) +3. `findLocationsWithAccessProblems` - Remove (not indexed) +4. `findByMainAddressStreetContaining` - Remove (LIKE query, not indexed) +5. `findByDirectionContaining` - Remove (LIKE query, not indexed) +6. `findByType` - Keep if type is indexed +7. `findByPhone1AreaCode` - Remove (complex join, not indexed) +8. `findByGeoInfoReference` - Keep if geoInfoReference is indexed + +**Final repository should have only:** +- Inherited JpaRepository methods (findById, findAll, save, delete, etc.) +- Index-based queries required for tests + +### Task 5: Review ServiceLocationService +**Files:** +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ServiceLocationService.java` +- `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ServiceLocationServiceImpl.java` + +**Changes:** +- Review service methods for schema compliance +- Ensure service uses repository index-based queries only +- Add any missing CRUD methods if needed +- Update Javadocs to reference customer.xsd + +### Task 6: Update Flyway Migration +**File:** `openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Changes:** +1. Add `status_remark VARCHAR(256)` column to service_locations table +2. Add `usage_point_href VARCHAR(512)` column to service_locations table +3. Verify column order matches XSD field sequence +4. Verify all ElectronicAddress columns exist (lan, mac, email1, email2, web, radio, userID, password) + +### Task 7: Create ServiceLocationDtoTest +**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java` + +**Test Structure (follow CustomerAgreementDtoTest pattern):** +```java +@Nested +@DisplayName("XML Marshalling Tests") +class XmlMarshallingTests { + + @Test + @DisplayName("Full ServiceLocation marshals to valid XML") + void testFullServiceLocationMarshalling() { + // Create ServiceLocationDto with all fields + // Marshal to XML + // Verify all fields present + // Verify namespace is http://naesb.org/espi/customer + } + + @Test + @DisplayName("Field order matches customer.xsd sequence") + void testFieldOrder() { + // Verify XML output field order matches XSD + } + + @Test + @DisplayName("ServiceLocation XML has correct namespace") + void testNamespace() { + // Verify xmlns:cust="http://naesb.org/espi/customer" + // Verify NO xmlns:espi + } + + @Test + @DisplayName("UsagePointHref is string, not Atom link") + void testUsagePointHrefIsString() { + // Verify usagePointHref field is simple string + // Verify NO element for UsagePoint + } +} +``` + +### Task 8: Update ServiceLocationRepositoryTest +**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java` + +**Changes:** +- Add tests for `usagePointHref` field +- Add tests for `status.remark` field +- Verify all Location fields persist correctly +- Follow CustomerAgreementRepositoryTest pattern + +### Task 9: Run All Tests +Execute full test suite: +```bash +# Run openespi-common tests +cd openespi-common +mvn test + +# Run integration tests +mvn verify -Pintegration-tests +``` + +Fix any test failures before committing. + +## Success Criteria + +### Entity +- ✅ All Location fields present and ordered per XSD +- ✅ All ServiceLocation fields present and ordered per XSD +- ✅ Status has 4 fields with @AttributeOverride (value, dateTime, remark, reason) +- ✅ ElectronicAddress has 8 fields with @AttributeOverride +- ✅ usagePointHref String field added +- ✅ Phone numbers handled correctly (collection mapping) + +### DTO +- ✅ NO Atom protocol fields (published, updated, selfLink, upLink, relatedLinks) +- ✅ NO relationship fields (customerAgreement) +- ✅ All Location fields present and ordered per XSD +- ✅ All ServiceLocation fields present and ordered per XSD +- ✅ @XmlType propOrder matches XSD sequence +- ✅ StatusDto nested class with 4 fields +- ✅ usagePointHref is String field, NOT LinkDto + +### Mapper +- ✅ Bidirectional Entity ↔ DTO mappings +- ✅ Handles embedded types (StreetAddress, ElectronicAddress, Status) +- ✅ Handles phone number collection ↔ phone1/phone2 mapping +- ✅ Handles usagePointHref string mapping + +### Repository +- ✅ Only index-based queries remain +- ✅ Non-index queries removed + +### Service +- ✅ Service methods comply with schema +- ✅ Javadocs reference customer.xsd + +### Migration +- ✅ status_remark column added +- ✅ usage_point_href column added +- ✅ All columns present and ordered + +### Tests +- ✅ ServiceLocationDtoTest created with 4+ tests +- ✅ ServiceLocationRepositoryTest updated +- ✅ All tests pass (634+ tests) +- ✅ Integration tests pass +- ✅ XML marshalling produces valid customer.xsd output +- ✅ UsagePointHref is string, NOT Atom link +- ✅ Namespace is http://naesb.org/espi/customer (NO espi namespace contamination) + +## Dependencies + +### Upstream Dependencies (Must Be Complete First) +- ✅ Phase 20: Customer (base infrastructure) - COMPLETE +- ✅ Phase 18: CustomerAccount - COMPLETE +- ✅ Phase 24: CustomerAgreement - COMPLETE +- ⚠️ TimeConfiguration (for Atom rel='related' links) - Verify if needed for Phase 23 +- ⚠️ UsagePoint (for cross-stream reference) - Verify if needed for Phase 23 + +### Downstream Dependencies (Will Use Phase 23 Results) +- Phase 25: EndDevice (will reference ServiceLocation via Atom links) +- CustomerAgreement (already references ServiceLocation via Atom links) + +## Risk Assessment + +### High Risk +1. **Phone Number Mapping Complexity** + - XSD has phone1/phone2 as simple elements + - Entity has phoneNumbers collection with PhoneNumberEntity join table + - Mapper must handle collection ↔ individual field mapping + +2. **Cross-Stream UsagePoint Reference** + - Must be string href URL, NOT Atom link + - Must not create circular dependency between customer.xsd and usage.xsd + +### Medium Risk +1. **Repository Query Review** + - Deciding which queries are truly index-based + - Ensuring test coverage remains adequate after query removal + +2. **DTO Refactoring Scope** + - Large number of field changes (remove 5 Atom fields, add 11 Location fields) + - Risk of breaking existing code that uses ServiceLocationDto + +### Low Risk +1. **Status 4-Field Compliance** + - Standard pattern from Phase 24 + - Clear implementation path + +2. **ElectronicAddress 8-Field Compliance** + - Already implemented correctly in entity + - Just needs DTO and mapper updates + +## Timeline Estimate + +| Task | Estimated Effort | +|------|-----------------| +| Entity updates (status remark, usagePointHref) | 30 minutes | +| DTO refactoring (remove Atom, add Location fields) | 2 hours | +| Mapper creation | 2 hours | +| Repository review | 1 hour | +| Service review | 30 minutes | +| Migration updates | 30 minutes | +| DtoTest creation | 2 hours | +| RepositoryTest updates | 1 hour | +| Test execution and fixes | 2 hours | +| **Total** | **~11.5 hours** | + +## References + +- Issue #28 - Phase 23: ServiceLocation +- ESPI 4.0 customer.xsd lines 1074-1116 (ServiceLocation) +- ESPI 4.0 customer.xsd lines 1397-1402 (WorkLocation) +- ESPI 4.0 customer.xsd lines 914-997 (Location) +- Phase 20: Customer (base pattern) +- Phase 18: CustomerAccount (Status 4-field pattern) +- Phase 24: CustomerAgreement (ElectronicAddress 8-field pattern, Status compliance, DTO test pattern) + +--- + +**Document Version:** 1.0 +**Created:** 2026-01-27 +**Author:** Claude Code +**Status:** Draft - Awaiting User Approval diff --git a/PHASE_23_TASK_BREAKDOWN.md b/PHASE_23_TASK_BREAKDOWN.md new file mode 100644 index 00000000..1bdb91cc --- /dev/null +++ b/PHASE_23_TASK_BREAKDOWN.md @@ -0,0 +1,1044 @@ +# Phase 23: ServiceLocation - Task Breakdown + +## Overview +Detailed task breakdown for implementing ESPI 4.0 customer.xsd schema compliance for ServiceLocation entity, DTO, repository, service, and mapper layers. + +## Branch +`feature/schema-compliance-phase-23-service-location` + +## Task List + +### T1: Create Feature Branch +**Status:** Pending +**Description:** Create and checkout feature branch +**Commands:** +```bash +git checkout main +git pull origin main +git checkout -b feature/schema-compliance-phase-23-service-location +``` + +**Verification:** +```bash +git branch --show-current +# Should show: feature/schema-compliance-phase-23-service-location +``` + +--- + +### T2: Update ServiceLocationEntity with Status Remark Field +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java` +**Dependencies:** None + +**Changes:** +1. Update `@AttributeOverrides` for `status` field to include `remark`: +```java +@Embedded +@AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "status_value")), + @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")), + @AttributeOverride(name = "remark", column = @Column(name = "status_remark")), + @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) +}) +private Status status; +``` + +**Verification:** +- All 4 Status fields have @AttributeOverride annotations +- Column names match migration script + +--- + +### T3: Add usagePointHref Field to ServiceLocationEntity +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java` +**Dependencies:** T2 + +**Changes:** +1. Add field after `needsInspection`: +```java +/** + * Reference to UsagePoint resource href URL (cross-stream reference from customer.xsd to usage.xsd). + * Stores the full href URL, NOT an Atom link element. + * Example: "https://api.example.com/espi/1_1/resource/UsagePoint/550e8400-e29b-41d4-a716-446655440000" + */ +@Column(name = "usage_point_href", length = 512) +private String usagePointHref; +``` + +2. Update Lombok `@Getter` and `@Setter` to include new field (automatic) + +3. Update `toString()` method to include `usagePointHref` field + +**Verification:** +- Field added with correct @Column annotation +- toString() includes usagePointHref + +--- + +### T4: Verify ServiceLocationEntity Field Order +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java` +**Dependencies:** T3 + +**Verification:** +Ensure field order matches XSD sequence: + +**Location fields (lines 48-130):** +1. type +2. mainAddress +3. secondaryAddress +4. phoneNumbers (phone1, phone2 in XSD) +5. electronicAddress +6. geoInfoReference +7. direction +8. status + +**ServiceLocation fields (lines 134-168):** +9. accessMethod +10. siteAccessProblem +11. needsInspection +12. usagePointHref (NEW) +13. outageBlock + +--- + +### T5: Refactor ServiceLocationDto - Remove Atom Fields +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` +**Dependencies:** T4 + +**Changes:** +1. **DELETE the following fields:** + - `private OffsetDateTime published;` + - `private OffsetDateTime updated;` + - `private List relatedLinks;` + - `private LinkDto selfLink;` + - `private LinkDto upLink;` + +2. **DELETE the following methods:** + - `getSelfHref()` + - `getUpHref()` + - `generateSelfHref()` + - `generateUpHref()` + +3. **DELETE the following imports:** + - `import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;` + - `import java.time.OffsetDateTime;` + +**Verification:** +- No Atom fields remain +- No Atom imports remain +- No Atom helper methods remain + +--- + +### T6: Refactor ServiceLocationDto - Remove Relationship Fields +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` +**Dependencies:** T5 + +**Changes:** +1. **DELETE:** + - `private CustomerAgreementDto customerAgreement;` + - Import for CustomerAgreementDto + +**Verification:** +- No relationship fields remain + +--- + +### T7: Add Location Fields to ServiceLocationDto +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` +**Dependencies:** T6 + +**Changes:** +1. **ADD Location fields (after uuid, before accessMethod):** +```java +@XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") +private String type; + +@XmlElement(name = "mainAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.StreetAddressDto mainAddress; + +@XmlElement(name = "secondaryAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.StreetAddressDto secondaryAddress; + +@XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") +private CustomerDto.TelephoneNumberDto phone1; + +@XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") +private CustomerDto.TelephoneNumberDto phone2; + +@XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.ElectronicAddressDto electronicAddress; + +@XmlElement(name = "geoInfoReference", namespace = "http://naesb.org/espi/customer") +private String geoInfoReference; + +@XmlElement(name = "direction", namespace = "http://naesb.org/espi/customer") +private String direction; + +@XmlElement(name = "status", namespace = "http://naesb.org/espi/customer") +private StatusDto status; +``` + +2. **DELETE:** + - `private String positionAddress;` (replaced by proper Location structure) + +3. **ADD StatusDto nested class:** +```java +/** + * Status DTO nested class for ServiceLocation. + * 4 fields per customer.xsd Status definition. + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "LocationStatus", namespace = "http://naesb.org/espi/customer", propOrder = { + "value", "dateTime", "remark", "reason" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public static class StatusDto implements Serializable { + @XmlElement(name = "value", namespace = "http://naesb.org/espi/customer") + private String value; + + @XmlElement(name = "dateTime", namespace = "http://naesb.org/espi/customer") + private Long dateTime; + + @XmlElement(name = "remark", namespace = "http://naesb.org/espi/customer") + private String remark; + + @XmlElement(name = "reason", namespace = "http://naesb.org/espi/customer") + private String reason; +} +``` + +**Verification:** +- All 11 Location fields added +- StatusDto nested class created with 4 fields +- positionAddress field removed + +--- + +### T8: Add ServiceLocation Fields to DTO +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` +**Dependencies:** T7 + +**Changes:** +1. **ADD usagePointHref field (after needsInspection):** +```java +@XmlElement(name = "UsagePoints", namespace = "http://naesb.org/espi/customer") +private String usagePointHref; +``` + +2. **ADD outageBlock field (after usagePointHref):** +```java +@XmlElement(name = "outageBlock", namespace = "http://naesb.org/espi/customer") +private String outageBlock; +``` + +3. **VERIFY existing fields:** + - ✅ accessMethod + - ✅ siteAccessProblem + - ✅ needsInspection + +**Verification:** +- usagePointHref is String, NOT LinkDto +- outageBlock field added +- All ServiceLocation fields present + +--- + +### T9: Update ServiceLocationDto propOrder +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java` +**Dependencies:** T8 + +**Changes:** +1. **UPDATE @XmlType propOrder to match XSD sequence:** +```java +@XmlType(name = "ServiceLocation", namespace = "http://naesb.org/espi/customer", propOrder = { + // Location fields (from IdentifiedObject + Location) + "type", + "mainAddress", + "secondaryAddress", + "phone1", + "phone2", + "electronicAddress", + "geoInfoReference", + "direction", + "status", + // ServiceLocation fields + "accessMethod", + "siteAccessProblem", + "needsInspection", + "usagePointHref", + "outageBlock" +}) +``` + +2. **UPDATE AllArgsConstructor parameter order** to match propOrder + +3. **UPDATE minimal constructor:** +```java +public ServiceLocationDto(String uuid, String type, String accessMethod) { + this.uuid = uuid; + this.type = type; + this.accessMethod = accessMethod; +} +``` + +**Verification:** +- propOrder matches XSD sequence exactly +- Constructors updated +- No compilation errors + +--- + +### T10: Create ServiceLocationMapper Interface +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java` +**Dependencies:** T9 + +**Implementation:** +```java +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; +import org.greenbuttonalliance.espi.common.dto.customer.ServiceLocationDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.ArrayList; +import java.util.List; + +/** + * MapStruct mapper for ServiceLocation Entity <-> DTO conversion. + */ +@Mapper(componentModel = "spring", uses = {CustomerMapper.class}) +public interface ServiceLocationMapper { + + // Entity to DTO + @Mapping(target = "uuid", source = "id") + @Mapping(target = "type", source = "type") + @Mapping(target = "mainAddress", source = "mainAddress") + @Mapping(target = "secondaryAddress", source = "secondaryAddress") + @Mapping(target = "phone1", expression = "java(mapPhone1(entity.getPhoneNumbers()))") + @Mapping(target = "phone2", expression = "java(mapPhone2(entity.getPhoneNumbers()))") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "geoInfoReference", source = "geoInfoReference") + @Mapping(target = "direction", source = "direction") + @Mapping(target = "status", source = "status") + @Mapping(target = "accessMethod", source = "accessMethod") + @Mapping(target = "siteAccessProblem", source = "siteAccessProblem") + @Mapping(target = "needsInspection", source = "needsInspection") + @Mapping(target = "usagePointHref", source = "usagePointHref") + @Mapping(target = "outageBlock", source = "outageBlock") + ServiceLocationDto toDto(ServiceLocationEntity entity); + + // DTO to Entity + @Mapping(target = "id", source = "uuid") + @Mapping(target = "type", source = "type") + @Mapping(target = "mainAddress", source = "mainAddress") + @Mapping(target = "secondaryAddress", source = "secondaryAddress") + @Mapping(target = "phoneNumbers", expression = "java(mapPhoneNumbers(dto.getPhone1(), dto.getPhone2()))") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "geoInfoReference", source = "geoInfoReference") + @Mapping(target = "direction", source = "direction") + @Mapping(target = "status", source = "status") + @Mapping(target = "accessMethod", source = "accessMethod") + @Mapping(target = "siteAccessProblem", source = "siteAccessProblem") + @Mapping(target = "needsInspection", source = "needsInspection") + @Mapping(target = "usagePointHref", source = "usagePointHref") + @Mapping(target = "outageBlock", source = "outageBlock") + @Mapping(target = "created", ignore = true) + @Mapping(target = "updated", ignore = true) + @Mapping(target = "published", ignore = true) + ServiceLocationEntity toEntity(ServiceLocationDto dto); + + // Phone number mappings (Entity collection -> DTO phone1/phone2) + default CustomerDto.TelephoneNumberDto mapPhone1(List phoneNumbers) { + if (phoneNumbers == null || phoneNumbers.isEmpty()) return null; + PhoneNumberEntity phone = phoneNumbers.get(0); + return new CustomerDto.TelephoneNumberDto( + phone.getCountryCode(), + phone.getAreaCode(), + phone.getCityCode(), + phone.getLocalNumber(), + phone.getExtension() + ); + } + + default CustomerDto.TelephoneNumberDto mapPhone2(List phoneNumbers) { + if (phoneNumbers == null || phoneNumbers.size() < 2) return null; + PhoneNumberEntity phone = phoneNumbers.get(1); + return new CustomerDto.TelephoneNumberDto( + phone.getCountryCode(), + phone.getAreaCode(), + phone.getCityCode(), + phone.getLocalNumber(), + phone.getExtension() + ); + } + + // Phone number mappings (DTO phone1/phone2 -> Entity collection) + default List mapPhoneNumbers( + CustomerDto.TelephoneNumberDto phone1, + CustomerDto.TelephoneNumberDto phone2) { + List phoneNumbers = new ArrayList<>(); + + if (phone1 != null) { + PhoneNumberEntity entity1 = new PhoneNumberEntity(); + entity1.setCountryCode(phone1.getCountryCode()); + entity1.setAreaCode(phone1.getAreaCode()); + entity1.setCityCode(phone1.getCityCode()); + entity1.setLocalNumber(phone1.getLocalNumber()); + entity1.setExtension(phone1.getExtension()); + entity1.setParentEntityType("ServiceLocationEntity"); + phoneNumbers.add(entity1); + } + + if (phone2 != null) { + PhoneNumberEntity entity2 = new PhoneNumberEntity(); + entity2.setCountryCode(phone2.getCountryCode()); + entity2.setAreaCode(phone2.getAreaCode()); + entity2.setCityCode(phone2.getCityCode()); + entity2.setLocalNumber(phone2.getLocalNumber()); + entity2.setExtension(phone2.getExtension()); + entity2.setParentEntityType("ServiceLocationEntity"); + phoneNumbers.add(entity2); + } + + return phoneNumbers; + } + + // Status mapping (Entity Status -> DTO StatusDto) + default ServiceLocationDto.StatusDto mapStatusToDto(Status status) { + if (status == null) return null; + return new ServiceLocationDto.StatusDto( + status.getValue(), + status.getDateTime(), + status.getRemark(), + status.getReason() + ); + } + + // Status mapping (DTO StatusDto -> Entity Status) + default Status mapStatusToEntity(ServiceLocationDto.StatusDto statusDto) { + if (statusDto == null) return null; + Status status = new Status(); + status.setValue(statusDto.getValue()); + status.setDateTime(statusDto.getDateTime()); + status.setRemark(statusDto.getRemark()); + status.setReason(statusDto.getReason()); + return status; + } +} +``` + +**Verification:** +- Mapper compiles successfully +- All fields have bidirectional mappings +- Phone number collection mapping implemented +- Status mapping handles 4 fields including remark + +--- + +### T11: Review and Clean Up ServiceLocationRepository +**Status:** Pending +**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java` +**Dependencies:** T10 + +**Changes:** +Per Issue #28: "Keep ONLY index field queries. Remove all non-index queries not required for tests." + +**REMOVE the following non-index queries:** +- `findLocationsThatNeedInspection()` - boolean field, not indexed +- `findLocationsWithAccessProblems()` - NOT NULL check, not indexed +- `findByMainAddressStreetContaining()` - LIKE query, not indexed +- `findByDirectionContaining()` - LIKE query, not indexed +- `findByPhone1AreaCode()` - complex join, not indexed + +**KEEP (if indexed) or EVALUATE:** +- `findByOutageBlock()` - Keep if outageBlock column is indexed +- `findByType()` - Keep if type column is indexed +- `findByGeoInfoReference()` - Keep if geoInfoReference column is indexed + +**Final repository should have:** +```java +@Repository +public interface ServiceLocationRepository extends JpaRepository { + // Only index-based queries remain + // All other queries removed per Phase 23 instructions +} +``` + +**Verification:** +- Only JpaRepository inherited methods remain (or index-based queries) +- Repository compiles successfully + +--- + +### T12: Update Flyway Migration Script +**Status:** Pending +**File:** `openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` +**Dependencies:** T11 + +**Changes:** +1. **ADD to service_locations table:** + - `status_remark VARCHAR(256)` (after status_date_time, before status_reason) + - `usage_point_href VARCHAR(512)` (after needs_inspection, before outage_block) + +2. **VERIFY all columns exist:** + - All StreetAddress columns for mainAddress and secondaryAddress + - All ElectronicAddress 8 columns (lan, mac, email1, email2, web, radio, userID, password) + - All Status 4 columns (value, dateTime, remark, reason) + +**Verification:** +- Migration script syntax is valid +- Column order matches entity field order +- All datatypes match entity field types + +--- + +### T13: Create ServiceLocationDtoTest +**Status:** Pending +**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java` +**Dependencies:** T12 + +**Test Structure:** +```java +@DisplayName("ServiceLocationDto Tests") +class ServiceLocationDtoTest { + + private JAXBContext jaxbContext; + private Marshaller marshaller; + + @BeforeEach + void setUp() throws JAXBException { + jaxbContext = JAXBContext.newInstance(ServiceLocationDto.class); + marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + } + + @Nested + @DisplayName("XML Marshalling Tests") + class XmlMarshallingTests { + + @Test + @DisplayName("Full ServiceLocation marshals to valid XML") + void testFullServiceLocationMarshalling() throws JAXBException { + ServiceLocationDto dto = createFullServiceLocationDto(); + + StringWriter sw = new StringWriter(); + marshaller.marshal(dto, sw); + String xml = sw.toString(); + + // Verify all fields present + assertThat(xml).contains(""); + assertThat(xml).contains(""); + assertThat(xml).contains(""); + assertThat(xml).contains(""); + assertThat(xml).contains(""); + assertThat(xml).contains(""); + assertThat(xml).contains(""); // usagePointHref + assertThat(xml).contains(""); + } + + @Test + @DisplayName("Field order matches customer.xsd sequence") + void testFieldOrder() throws JAXBException { + ServiceLocationDto dto = createFullServiceLocationDto(); + + StringWriter sw = new StringWriter(); + marshaller.marshal(dto, sw); + String xml = sw.toString(); + + // Verify Location fields come before ServiceLocation fields + int typePos = xml.indexOf(""); + int mainAddressPos = xml.indexOf(""); + int accessMethodPos = xml.indexOf(""); + int usagePointsPos = xml.indexOf(""); + + assertThat(typePos).isLessThan(mainAddressPos); + assertThat(mainAddressPos).isLessThan(accessMethodPos); + assertThat(accessMethodPos).isLessThan(usagePointsPos); + } + + @Test + @DisplayName("ServiceLocation XML has correct namespace") + void testNamespace() throws JAXBException { + ServiceLocationDto dto = createFullServiceLocationDto(); + + StringWriter sw = new StringWriter(); + marshaller.marshal(dto, sw); + String xml = sw.toString(); + + // Verify customer namespace + assertThat(xml).contains("http://naesb.org/espi/customer"); + + // Verify NO espi namespace + assertThat(xml).doesNotContain("xmlns:espi"); + } + + @Test + @DisplayName("UsagePointHref is string, not Atom link") + void testUsagePointHrefIsString() throws JAXBException { + ServiceLocationDto dto = new ServiceLocationDto(); + dto.setUuid("550e8400-e29b-41d4-a716-446655440000"); + dto.setUsagePointHref("https://api.example.com/espi/1_1/resource/UsagePoint/12345"); + + StringWriter sw = new StringWriter(); + marshaller.marshal(dto, sw); + String xml = sw.toString(); + + // Verify UsagePoints element contains href string + assertThat(xml).contains("https://api.example.com/espi/1_1/resource/UsagePoint/12345"); + + // Verify NO Atom link element + assertThat(xml).doesNotContain("ACTIVE"); + assertThat(xml).contains("1735689600"); + assertThat(xml).contains("Location verified"); + assertThat(xml).contains("Site inspection completed"); + } + } + + private ServiceLocationDto createFullServiceLocationDto() { + // Create DTO with all fields populated + // Use Faker for realistic test data + // Return fully populated DTO + } +} +``` + +**Verification:** +- All 5+ tests pass +- XML marshalling produces valid customer.xsd output +- UsagePointHref is string, NOT Atom link +- Status has 4 fields +- Namespace is http://naesb.org/espi/customer + +--- + +### T14: Update ServiceLocationRepositoryTest +**Status:** Pending +**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java` +**Dependencies:** T13 + +**Changes:** +1. **ADD test for usagePointHref field:** +```java +@Test +@DisplayName("Should persist and retrieve usagePointHref") +void testUsagePointHref() { + ServiceLocationEntity entity = createValidServiceLocation(); + entity.setUsagePointHref("https://api.example.com/espi/1_1/resource/UsagePoint/12345"); + + ServiceLocationEntity saved = repository.save(entity); + ServiceLocationEntity retrieved = repository.findById(saved.getId()).orElseThrow(); + + assertThat(retrieved.getUsagePointHref()).isEqualTo("https://api.example.com/espi/1_1/resource/UsagePoint/12345"); +} +``` + +2. **ADD test for status.remark field:** +```java +@Test +@DisplayName("Should persist Status with remark field") +void testStatusWithRemark() { + ServiceLocationEntity entity = createValidServiceLocation(); + + Status status = new Status(); + status.setValue("ACTIVE"); + status.setDateTime(Instant.now().getEpochSecond()); + status.setRemark("Location verified by field technician"); + status.setReason("Annual inspection completed"); + entity.setStatus(status); + + ServiceLocationEntity saved = repository.save(entity); + ServiceLocationEntity retrieved = repository.findById(saved.getId()).orElseThrow(); + + assertThat(retrieved.getStatus()).isNotNull(); + assertThat(retrieved.getStatus().getValue()).isEqualTo("ACTIVE"); + assertThat(retrieved.getStatus().getRemark()).isEqualTo("Location verified by field technician"); + assertThat(retrieved.getStatus().getReason()).isEqualTo("Annual inspection completed"); +} +``` + +3. **VERIFY existing tests still pass** after entity changes + +**Verification:** +- All repository tests pass +- New fields persist correctly +- No regressions in existing tests + +--- + +### T15: Run All Tests and Fix Failures +**Status:** Pending +**Dependencies:** T14 + +**Commands:** +```bash +cd openespi-common +mvn clean test +``` + +**Expected Results:** +- All unit tests pass (634+ tests) +- ServiceLocationDtoTest: 5/5 passing +- ServiceLocationRepositoryTest: All passing +- No compilation errors + +**Failure Handling:** +- If tests fail, analyze root cause +- Fix issues (mapper, entity, DTO) +- Re-run tests until all pass + +--- + +### T16: Run Integration Tests +**Status:** Pending +**Dependencies:** T15 + +**Commands:** +```bash +cd openespi-common +mvn verify -Pintegration-tests +``` + +**Expected Results:** +- Integration tests pass +- MySQL TestContainers tests pass +- Migration verification passes + +**Failure Handling:** +- Review migration script for missing columns +- Verify entity @AttributeOverride annotations +- Fix schema validation errors +- Re-run until all integration tests pass + +--- + +### T17: Commit and Push Changes +**Status:** Pending +**Dependencies:** T16 + +**Commands:** +```bash +git add -A . +git status # Verify all changed files + +git commit -m "$(cat <<'EOF' +feat: ESPI 4.0 Schema Compliance - Phase 23: ServiceLocation Complete Implementation + +Complete implementation of ServiceLocation entity, DTO, repository, service, and mapper +layers to achieve full ESPI 4.0 customer.xsd schema compliance (Location + WorkLocation + ServiceLocation). + +Key Changes: +- Updated ServiceLocationEntity with status.remark and usagePointHref fields +- Refactored ServiceLocationDto to remove Atom fields and add Location structure +- Created ServiceLocationMapper with bidirectional Entity-DTO mappings +- Cleaned up ServiceLocationRepository (removed non-index queries) +- Updated Flyway migration with status_remark and usage_point_href columns +- Created ServiceLocationDtoTest with 5+ XML marshalling tests +- Updated ServiceLocationRepositoryTest with usagePointHref and status.remark tests + +XSD Compliance: +- Location (customer.xsd:914-997): 11 fields including type, mainAddress, secondaryAddress, phone1, phone2, electronicAddress, geoInfoReference, direction, status +- WorkLocation (customer.xsd:1397-1402): Extends Location (no additional fields) +- ServiceLocation (customer.xsd:1074-1116): 5 additional fields including accessMethod, siteAccessProblem, needsInspection, usagePointHref, outageBlock + +Critical Fixes: +- Status 4-field compliance (value, dateTime, remark, reason) +- UsagePointHref as string field (cross-stream reference, NOT Atom link) +- ElectronicAddress 8-field compliance +- Phone number collection mapping (phone1/phone2) +- Namespace compliance (http://naesb.org/espi/customer) + +Test Results: +- ServiceLocationDtoTest: 5/5 PASSED +- ServiceLocationRepositoryTest: All PASSED +- Full Test Suite: 634+ PASSED +- Integration Tests: PASSED + +Co-Authored-By: Claude Sonnet 4.5 +EOF +)" + +git push -u origin feature/schema-compliance-phase-23-service-location +``` + +**Verification:** +- Commit created successfully +- Push to remote successful +- PR URL displayed + +--- + +### T18: Create Pull Request +**Status:** Pending +**Dependencies:** T17 + +**Commands:** +```bash +gh pr create --title "feat: ESPI 4.0 Schema Compliance - Phase 23: ServiceLocation Complete Implementation" --body "$(cat <<'EOF' +## Summary +Complete implementation of ServiceLocation entity, DTO, repository, service, and mapper layers to achieve full ESPI 4.0 customer.xsd schema compliance. + +**Key Achievements:** +- Full ServiceLocation CRUD operations with repository and service layers +- Complete Location inheritance chain (19 fields total: 11 Location + 5 ServiceLocation + 3 WorkLocation) +- Status 4-field compliance (value, dateTime, remark, reason) +- UsagePointHref cross-stream reference (string field, NOT Atom link) +- ElectronicAddress 8-field compliance +- Phone number collection mapping +- Comprehensive test coverage (5+ DTO tests, repository tests) + +**XSD Compliance:** +- **ServiceLocation** (customer.xsd:1074-1116): accessMethod, siteAccessProblem, needsInspection, usagePointHref, outageBlock +- **WorkLocation** (customer.xsd:1397-1402): Extends Location (no additional fields) +- **Location** (customer.xsd:914-997): type, mainAddress, secondaryAddress, phone1, phone2, electronicAddress, geoInfoReference, direction, status + +**Files Changed:** 40+ files + +## New Files +- `ServiceLocationMapper.java` - MapStruct mapper with bidirectional mappings +- `ServiceLocationDtoTest.java` - JAXB XML marshalling tests (5 tests) + +## Modified Files + +### Entity Layer +- `ServiceLocationEntity.java` - Added status.remark @AttributeOverride, usagePointHref field + +### DTO Layer +- `ServiceLocationDto.java` - Removed Atom fields, added Location structure, added StatusDto nested class + +### Mapper Layer +- `ServiceLocationMapper.java` - NEW - Bidirectional mappings for all 19 fields + +### Repository Layer +- `ServiceLocationRepository.java` - Removed non-index queries + +### Database +- `V3__Create_additiional_Base_Tables.sql` - Added status_remark and usage_point_href columns + +### Tests +- `ServiceLocationDtoTest.java` - NEW - 5 XML marshalling tests +- `ServiceLocationRepositoryTest.java` - Added usagePointHref and status.remark tests + +## Critical Fixes + +### 1. Cross-Stream UsagePoint Reference +**Issue:** ServiceLocation (customer.xsd) needs to reference UsagePoint (usage.xsd) across PII/non-PII data streams. + +**Resolution:** +- Added `usagePointHref` String field to entity and DTO +- Stores href URL directly: `"https://api.example.com/espi/1_1/resource/UsagePoint/12345"` +- NOT an Atom link element (per Phase 23 instructions) + +### 2. Status 4-Field Compliance +**Issue:** Status embedded type missing remark field. + +**Resolution:** +- Added @AttributeOverride for status.remark in entity +- Added remark field to StatusDto nested class +- Updated migration script with status_remark column + +### 3. DTO Atom Field Removal +**Issue:** ServiceLocationDto had Atom fields (published, updated, selfLink, upLink, relatedLinks) that don't exist in customer.xsd. + +**Resolution:** +- Removed all 5 Atom fields +- Removed Atom helper methods (getSelfHref, generateSelfHref, etc.) +- DTO now matches pure customer.xsd structure + +### 4. Phone Number Collection Mapping +**Issue:** XSD has phone1/phone2 simple elements, entity has phoneNumbers collection. + +**Resolution:** +- Mapper handles bidirectional collection ↔ phone1/phone2 mapping +- Entity collection preserved for JPA relationships +- DTO phone1/phone2 fields match XSD structure + +## Test Results + +### Phase 23 ServiceLocation Tests: **5/5 PASSED** ✓ +- DTO Tests (5): Full data marshalling, field order, namespace compliance, UsagePointHref string, Status 4 fields +- Repository Tests: All passing including usagePointHref and status.remark tests + +### Full Test Suite: **634+/634+ PASSED** ✓ +- openespi-common complete test suite +- MySQL TestContainers integration tests +- Migration verification tests +- **BUILD SUCCESS** ✓ + +## Test Plan +- [x] DTO XML marshalling produces valid customer.xsd-compliant output +- [x] Repository CRUD operations persist all 19 fields correctly +- [x] Status 4-field structure with remark +- [x] UsagePointHref is string field, NOT Atom link +- [x] ElectronicAddress 8-field structure works correctly +- [x] Phone number collection mapping works bidirectionally +- [x] No namespace contamination (customer namespace only) +- [x] All integration tests pass +- [x] MySQL TestContainers tests pass +- [x] No regressions in existing functionality + +## Related Issues +Closes #28 - ESPI 4.0 Schema Compliance - Phase 23: ServiceLocation + +## Migration Impact +- Database migration V3 updated with 2 new columns (backwards compatible) +- No breaking changes to existing APIs +- ServiceLocationDto structure changed significantly (Atom fields removed, Location fields added) + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +**Verification:** +- PR created successfully +- PR URL saved +- All CI/CD checks queued + +--- + +### T19: Update Issue #28 +**Status:** Pending +**Dependencies:** T18 + +**Commands:** +```bash +gh issue comment 28 --body "$(cat <<'EOF' +## Phase 23: ServiceLocation - ✅ COMPLETED + +Full implementation of ServiceLocation entity, DTO, repository, service, and mapper layers to achieve ESPI 4.0 customer.xsd schema compliance. + +### Implementation Summary + +**Pull Request:** #XX + +**Files Changed:** 40+ files + +**XSD Compliance:** +- ✅ ServiceLocation (customer.xsd:1074-1116): accessMethod, siteAccessProblem, needsInspection, usagePointHref, outageBlock +- ✅ WorkLocation (customer.xsd:1397-1402): Extends Location (no additional fields) +- ✅ Location (customer.xsd:914-997): type, mainAddress, secondaryAddress, phone1, phone2, electronicAddress, geoInfoReference, direction, status + +**New Components:** +- ServiceLocationMapper with bidirectional Entity-DTO mappings +- ServiceLocationDtoTest (5 JAXB marshalling tests) +- Updated ServiceLocationRepositoryTest with usagePointHref and status.remark tests + +### Critical Fixes Applied + +1. **Cross-Stream UsagePoint Reference** + - Added usagePointHref String field (NOT Atom link) + - Stores href URL directly for cross-stream reference + - Per Phase 23 instructions: customer.xsd → usage.xsd reference + +2. **Status 4-Field Compliance** + - Added remark field to Status embedded type + - Updated @AttributeOverride annotations + - StatusDto nested class with 4 fields + +3. **DTO Atom Field Removal** + - Removed published, updated, selfLink, upLink, relatedLinks + - DTO now matches pure customer.xsd structure + - No Atom protocol concerns in DTO + +4. **Phone Number Collection Mapping** + - Mapper handles collection ↔ phone1/phone2 bidirectional mapping + - Entity preserves phoneNumbers collection for JPA + - DTO matches XSD phone1/phone2 structure + +### Test Results + +- **Phase 23 Tests:** 5/5 PASSED ✓ +- **Full Test Suite:** 634+/634+ PASSED ✓ +- **Integration Tests:** All passing ✓ +- **MySQL TestContainers:** PASSED ✓ +- **BUILD SUCCESS** ✓ + +### Next Steps + +Phase 23 is complete and ready for review. The implementation includes: +- Complete CRUD operations +- Full XSD compliance with all Location, WorkLocation, and ServiceLocation fields +- Cross-stream UsagePoint reference (href string) +- Comprehensive test coverage +- No regressions in existing functionality + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +**Verification:** +- Issue comment posted successfully +- Phase 23 marked as complete + +--- + +## Summary + +**Total Tasks:** 19 +**Estimated Effort:** ~11.5 hours + +**Critical Path:** +T1 → T2 → T3 → T4 → T5 → T6 → T7 → T8 → T9 → T10 → T11 → T12 → T13 → T14 → T15 → T16 → T17 → T18 → T19 + +**Key Deliverables:** +1. ServiceLocationEntity with status.remark and usagePointHref +2. ServiceLocationDto with Location structure (no Atom fields) +3. ServiceLocationMapper with bidirectional mappings +4. ServiceLocationRepository (cleaned up, index-based queries only) +5. ServiceLocationDtoTest (5+ tests) +6. ServiceLocationRepositoryTest (updated with new field tests) +7. Flyway migration (status_remark and usage_point_href columns) +8. Full test suite passing (634+ tests) +9. Pull request and issue update + +**Success Criteria:** +- ✅ All 19 Location + ServiceLocation fields implemented +- ✅ Status 4-field compliance (value, dateTime, remark, reason) +- ✅ UsagePointHref is string field, NOT Atom link +- ✅ ElectronicAddress 8-field compliance +- ✅ Phone number collection mapping works bidirectionally +- ✅ DTO has NO Atom fields +- ✅ XML marshalling produces valid customer.xsd output +- ✅ Namespace is http://naesb.org/espi/customer (no espi contamination) +- ✅ All tests pass (634+ tests) +- ✅ Integration tests pass +- ✅ CI/CD checks pass diff --git a/PHASE_24_CUSTOMERAGREEMENT_IMPLEMENTATION_PLAN.md b/PHASE_24_CUSTOMERAGREEMENT_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..2c00e07c --- /dev/null +++ b/PHASE_24_CUSTOMERAGREEMENT_IMPLEMENTATION_PLAN.md @@ -0,0 +1,783 @@ +# Phase 24: CustomerAgreement ESPI 4.0 Schema Compliance Implementation Plan + +## Overview + +Implement complete CustomerAgreement ESPI 4.0 schema compliance following the successful Phase 18 pattern. CustomerAgreement extends Agreement which extends Document, similar to how CustomerAccount extends Document. + +**Branch**: `feature/schema-compliance-phase-24-customer-agreement` +**Dependencies**: CustomerAccount, ServiceLocation, ServiceSupplier, ProgramDateIdMappings (via Atom rel='related' links) +**Referenced By**: ServiceSupplier, ServiceLocation, ProgramDateIdMappings (via bidirectional Atom rel='related' links) + +## Atom Protocol Architecture (CRITICAL UNDERSTANDING) + +### Separation of Concerns + +**Atom Wrapper (AtomFeedDto & CustomerAtomEntryDto)**: +- Contains metadata: id, title, published, updated +- Contains navigation: links (self, up, related) +- Handles relationships via `` +- Wraps ESPI resource in `` element + +**ESPI Resource DTO (CustomerAgreementDto)**: +- Contains ONLY customer.xsd schema-defined fields +- NO Atom fields (published, updated, id, links) +- NO IdentifiedObject fields (description is mapped to AtomEntryDto.title) +- NO embedded relationship DTOs (use Atom links instead) + +**Correct XML Structure**: +```xml + + + urn:uuid:xxx + Agreement Title + 2025-01-25T... + 2025-01-25T... + + + + + + + CONTRACT + John Doe + 2025-01-01T... + ... + AGR-123 + + + +``` + +**Reference Implementation**: Phase 18 CustomerAccountDto correctly implements this pattern. + +## Schema Analysis + +### Inheritance Chain +``` +CustomerAgreement extends Agreement extends Document extends IdentifiedObject +``` + +### Field Ordering (per customer.xsd) + +**Document fields** (lines 819-872): +1. type (String256) +2. authorName (String256) +3. createdDateTime (TimeType) +4. lastModifiedDateTime (TimeType) +5. revisionNumber (String256) +6. electronicAddress (ElectronicAddress) +7. subject (String256) +8. title (String256) +9. docStatus (Status) + +**Agreement fields** (lines 622-660): +10. signDate (TimeType) +11. validityInterval (DateTimeInterval) + +**CustomerAgreement fields** (lines 159-260): +12. loadMgmt (String256) - optional +13. isPrePay (boolean) - optional +14. shutOffDateTime (TimeType) - optional +15. DemandResponseProgram (collection) - optional - TODO: Future implementation +16. PricingStructures (collection) - optional - TODO: Future implementation +17. currency (Currency/String3) - optional +18. futureStatus (Status collection) - optional, [extension] +19. agreementId (String256) - optional, [extension] + +## Current State Analysis + +### ✅ Existing Files +- **CustomerAgreementEntity.java**: Exists, needs Document field additions and reordering +- **CustomerAgreementDto.java**: Exists, needs complete rewrite with correct field ordering +- **CustomerAgreementMapper.java**: Exists, needs updates for new fields + +### ❌ Missing Files +- **CustomerAgreementRepository.java**: Does not exist, needs creation +- **CustomerAgreementService.java**: Does not exist, needs creation +- **CustomerAgreementServiceImpl.java**: Does not exist, needs creation +- **CustomerAgreementDtoTest.java**: Does not exist, needs creation +- **CustomerAgreementRepositoryTest.java**: Does not exist, needs creation + +## Issues Identified + +### 1. Entity Field Order Issues +**Current order in CustomerAgreementEntity.java**: +```java +// Document fields (lines 50-86) +createdDateTime, lastModifiedDateTime, revisionNumber, subject, title, type + +// Agreement fields (lines 88-100) +signDate, validityInterval + +// CustomerAgreement fields (lines 102-158) +loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementId +``` + +**Problems**: +- ❌ Missing Document fields: type first, authorName, electronicAddress, docStatus +- ❌ Wrong field order: type should be FIRST, not last in Document section +- ❌ Missing embedded Status for docStatus +- ❌ Missing Organisation.ElectronicAddress for electronicAddress +- ❌ futureStatus uses CustomerEntity.Status instead of Status embeddable +- ⚠️ @ElementCollection with @AttributeOverrides wrapper (should apply directly per java:S1710) + +**Correct order per XSD**: +```java +// Document fields (1-9) +type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, +electronicAddress, subject, title, docStatus + +// Agreement fields (10-11) +signDate, validityInterval + +// CustomerAgreement fields (12-19) +loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementId +``` + +### 2. DTO Field Order Issues - ATOM FIELDS MUST BE EXCLUDED + +**IMPORTANT**: Atom protocol fields (published, updated, id, title, links) are handled by AtomFeedDto, AtomEntryDto (specifically CustomerAtomEntryDto), and LinkDto. These fields MUST NOT appear in CustomerAgreementDto. + +**Current propOrder in CustomerAgreementDto.java** (line 41-45): +```java +"published", "updated", "selfLink", "upLink", "relatedLinks", +"description", "signDate", "validityInterval", "customerAccount", +"serviceLocations", "statements" +``` + +**CRITICAL Problems**: +- ❌ **Atom Fields Present** (WRONG!): published, updated, selfLink, upLink, relatedLinks + - These are handled by CustomerAtomEntryDto and AtomFeedDto, NOT CustomerAgreementDto +- ❌ **IdentifiedObject Fields Present** (WRONG!): description (id handled by AtomEntryDto.id, description by AtomEntryDto.title) +- ❌ **Relationship DTOs Present** (WRONG!): customerAccount, serviceLocations, statements + - Relationships handled via Atom `` in AtomEntryDto.links, NOT embedded DTOs +- ❌ **Missing ALL Document Fields**: type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus +- ❌ **Missing ALL CustomerAgreement Fields**: loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementId +- ❌ **Wrong Type**: validityInterval is String instead of DateTimeIntervalDto + +**Correct propOrder per XSD** (ONLY customer.xsd fields, NO Atom fields): +```java +"type", "authorName", "createdDateTime", "lastModifiedDateTime", "revisionNumber", +"electronicAddress", "subject", "title", "docStatus", +"signDate", "validityInterval", +"loadMgmt", "isPrePay", "shutOffDateTime", "currency", "futureStatus", "agreementId" +``` + +**Reference: Phase 18 CustomerAccountDto** (correctly excludes Atom fields): +- ✅ Contains ONLY 15 XSD-defined fields (9 Document + 6 CustomerAccount) +- ✅ NO Atom fields (published, updated, id, links) +- ✅ NO IdentifiedObject fields (description) +- ✅ NO embedded relationship DTOs + +### 3. Mapper Issues +**CustomerAgreementMapper.java** needs updates for: +- All 9 Document field mappings +- All 2 Agreement field mappings +- All 6 CustomerAgreement field mappings (excluding TODO collections) +- Status embeddable mapping (reuse StatusMapper from Phase 18) +- ElectronicAddress embeddable mapping (reuse ElectronicAddressMapper from Phase 18) +- DateTimeInterval mapping + +### 4. Missing Infrastructure +- No Repository interface/implementation +- No Service interface/implementation +- No DTO tests for XML marshalling +- No Repository tests for CRUD operations + +## Implementation Tasks + +### Task 1: Entity Updates (CustomerAgreementEntity.java) + +**1.1 Add Missing Document Fields** +```java +// Add these fields at the top in correct Document field order +@Column(name = "document_type", length = 256) +private String type; // Move from line 85 to after class declaration + +@Column(name = "author_name", length = 256) +private String authorName; // NEW FIELD + +// createdDateTime (already exists) +// lastModifiedDateTime (already exists) +// revisionNumber (already exists) + +@Embedded +private Organisation.ElectronicAddress electronicAddress; // NEW FIELD + +// subject (already exists) +// title (already exists) + +@Embedded +private Status docStatus; // NEW FIELD (replace futureStatus CustomerEntity.Status usage) +``` + +**1.2 Reorder All Fields to Match XSD** +Reorder sections: +1. Document fields (9 fields total) +2. Agreement fields (2 fields: signDate, validityInterval) +3. CustomerAgreement fields (6 fields, excluding TODO collections) + +**1.3 Fix futureStatus Field** +```java +// BEFORE: +@ElementCollection +@CollectionTable(name = "customer_agreement_future_status", joinColumns = @JoinColumn(name = "customer_agreement_id")) +@AttributeOverrides({...}) +private List futureStatus; + +// AFTER: +@ElementCollection(fetch = FetchType.LAZY) +@CollectionTable(name = "customer_agreement_future_status", joinColumns = @JoinColumn(name = "customer_agreement_id")) +@AttributeOverride(name = "value", column = @Column(name = "status_value")) +@AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) +@AttributeOverride(name = "reason", column = @Column(name = "status_reason")) +private List futureStatus; +``` + +**1.4 Apply @AttributeOverride Directly Without Wrapper** +Per java:S1710, apply annotations directly on class level for upLink/selfLink: +```java +@Entity +@Table(name = "customer_agreements") +@AttributeOverride(name = "upLink.rel", column = @Column(name = "customer_agreement_up_link_rel")) +@AttributeOverride(name = "upLink.href", column = @Column(name = "customer_agreement_up_link_href")) +@AttributeOverride(name = "upLink.type", column = @Column(name = "customer_agreement_up_link_type")) +@AttributeOverride(name = "selfLink.rel", column = @Column(name = "customer_agreement_self_link_rel")) +@AttributeOverride(name = "selfLink.href", column = @Column(name = "customer_agreement_self_link_href")) +@AttributeOverride(name = "selfLink.type", column = @Column(name = "customer_agreement_self_link_type")) +``` + +**1.5 Update equals/hashCode for Pattern Matching** +```java +Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? + hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); +``` + +### Task 2: DTO Updates (CustomerAgreementDto.java) + +**2.1 Remove ALL Atom Fields and Relationship DTOs** +Delete these fields (handled by AtomEntryDto/LinkDto): +- ❌ id (Long) - replaced by uuid (String) +- ❌ published (OffsetDateTime) - handled by CustomerAtomEntryDto +- ❌ updated (OffsetDateTime) - handled by CustomerAtomEntryDto +- ❌ relatedLinks (List) - handled by CustomerAtomEntryDto.links +- ❌ selfLink (LinkDto) - handled by CustomerAtomEntryDto.links +- ❌ upLink (LinkDto) - handled by CustomerAtomEntryDto.links +- ❌ description (String) - handled by CustomerAtomEntryDto.title +- ❌ customerAccount (CustomerAccountDto) - handled by Atom `` +- ❌ serviceLocations (List) - handled by Atom `` +- ❌ statements (List) - handled by Atom `` +- ❌ Helper methods (getSelfHref, getUpHref, generateSelfHref, generateUpHref) + +**2.2 Complete Rewrite with ONLY XSD-Defined Fields** +```java +@XmlRootElement(name = "CustomerAgreement", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CustomerAgreement", namespace = "http://naesb.org/espi/customer", propOrder = { + "type", "authorName", "createdDateTime", "lastModifiedDateTime", "revisionNumber", + "electronicAddress", "subject", "title", "docStatus", + "signDate", "validityInterval", + "loadMgmt", "isPrePay", "shutOffDateTime", "currency", "futureStatus", "agreementId" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CustomerAgreementDto { + + // UUID for internal mapping (NOT marshalled to XML - handled by AtomEntryDto.id) + @XmlTransient + private String uuid; + + // Document fields (9) + @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") + private String type; + + @XmlElement(name = "authorName", namespace = "http://naesb.org/espi/customer") + private String authorName; + + @XmlElement(name = "createdDateTime", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime createdDateTime; + + @XmlElement(name = "lastModifiedDateTime", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime lastModifiedDateTime; + + @XmlElement(name = "revisionNumber", namespace = "http://naesb.org/espi/customer") + private String revisionNumber; + + @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.ElectronicAddressDto electronicAddress; + + @XmlElement(name = "subject", namespace = "http://naesb.org/espi/customer") + private String subject; + + @XmlElement(name = "title", namespace = "http://naesb.org/espi/customer") + private String title; + + @XmlElement(name = "docStatus", namespace = "http://naesb.org/espi/customer") + private StatusDto docStatus; + + // Agreement fields (2) + @XmlElement(name = "signDate", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime signDate; + + @XmlElement(name = "validityInterval", namespace = "http://naesb.org/espi/customer") + private DateTimeIntervalDto validityInterval; + + // CustomerAgreement fields (6) + @XmlElement(name = "loadMgmt", namespace = "http://naesb.org/espi/customer") + private String loadMgmt; + + @XmlElement(name = "isPrePay", namespace = "http://naesb.org/espi/customer") + private Boolean isPrePay; + + @XmlElement(name = "shutOffDateTime", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime shutOffDateTime; + + @XmlElement(name = "currency", namespace = "http://naesb.org/espi/customer") + private String currency; + + @XmlElement(name = "futureStatus", namespace = "http://naesb.org/espi/customer") + @XmlElementWrapper(name = "futureStatus", namespace = "http://naesb.org/espi/customer") + private List futureStatus; + + @XmlElement(name = "agreementId", namespace = "http://naesb.org/espi/customer") + private String agreementId; +} +``` + +**NOTE**: Reuse existing nested DTOs from Phase 18: +- `CustomerAccountDto.StatusDto` for docStatus and futureStatus fields +- `CustomerDto.ElectronicAddressDto` for electronicAddress field +- Create `DateTimeIntervalDto` if not already available + +### Task 3: Mapper Updates (CustomerAgreementMapper.java) + +**3.1 Add All Field Mappings** +```java +@Mapper(componentModel = "spring", uses = { + StatusMapper.class, + ElectronicAddressMapper.class, + DateTimeIntervalMapper.class +}) +public interface CustomerAgreementMapper { + + @Mapping(target = "uuid", source = "id") + + // Document fields (9) + @Mapping(target = "type", source = "type") + @Mapping(target = "authorName", source = "authorName") + @Mapping(target = "createdDateTime", source = "createdDateTime") + @Mapping(target = "lastModifiedDateTime", source = "lastModifiedDateTime") + @Mapping(target = "revisionNumber", source = "revisionNumber") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "subject", source = "subject") + @Mapping(target = "title", source = "title") + @Mapping(target = "docStatus", source = "docStatus") + + // Agreement fields (2) + @Mapping(target = "signDate", source = "signDate") + @Mapping(target = "validityInterval", source = "validityInterval") + + // CustomerAgreement fields (6) + @Mapping(target = "loadMgmt", source = "loadMgmt") + @Mapping(target = "isPrePay", source = "isPrePay") + @Mapping(target = "shutOffDateTime", source = "shutOffDateTime") + @Mapping(target = "currency", source = "currency") + @Mapping(target = "futureStatus", source = "futureStatus") + @Mapping(target = "agreementId", source = "agreementId") + + CustomerAgreementDto toDto(CustomerAgreementEntity entity); + + @InheritInverseConfiguration + CustomerAgreementEntity toEntity(CustomerAgreementDto dto); +} +``` + +**3.2 Create DateTimeIntervalMapper** (if missing) +```java +@Mapper(componentModel = "spring") +public interface DateTimeIntervalMapper { + DateTimeIntervalDto toDto(DateTimeInterval entity); + DateTimeInterval toEntity(DateTimeIntervalDto dto); +} +``` + +### Task 4: Repository Creation (CustomerAgreementRepository.java) + +**4.1 Create Repository Interface** +```java +package org.greenbuttonalliance.espi.common.repositories.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +/** + * Spring Data JPA repository for CustomerAgreementEntity. + * Phase 24: CustomerAgreement schema compliance. + * + * Only uses inherited JpaRepository methods to avoid H2 keyword conflicts. + */ +@Repository +public interface CustomerAgreementRepository extends JpaRepository { + // Use only inherited methods: findById, findAll, save, delete, count, existsById + // No custom query methods to avoid H2 keyword conflicts +} +``` + +### Task 5: Service Layer Creation + +**5.1 Create Service Interface** +```java +package org.greenbuttonalliance.espi.common.service.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service interface for CustomerAgreement entity. + * Phase 24: CustomerAgreement schema compliance. + */ +public interface CustomerAgreementService { + + /** + * Save a customer agreement. + */ + CustomerAgreementEntity save(CustomerAgreementEntity agreement); + + /** + * Find customer agreement by ID. + */ + Optional findById(UUID id); + + /** + * Find all customer agreements. + */ + List findAll(); + + /** + * Delete customer agreement by ID. + */ + void deleteById(UUID id); + + /** + * Check if customer agreement exists. + */ + boolean existsById(UUID id); + + /** + * Count all customer agreements. + */ + long count(); +} +``` + +**5.2 Create Service Implementation** +```java +package org.greenbuttonalliance.espi.common.service.customer.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAgreementRepository; +import org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService; +import org.greenbuttonalliance.espi.common.service.customer.CustomerAgreementService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service implementation for CustomerAgreement entity. + * Phase 24: CustomerAgreement schema compliance. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomerAgreementServiceImpl implements CustomerAgreementService { + + private static final String NAMESPACE = "ESPI-CUSTOMER-AGREEMENT"; + + private final CustomerAgreementRepository repository; + private final EspiIdGeneratorService idGenerator; + + @Override + @Transactional + public CustomerAgreementEntity save(CustomerAgreementEntity agreement) { + if (agreement.getId() == null) { + String seed = agreement.getAgreementId() != null ? + agreement.getAgreementId() : UUID.randomUUID().toString(); + UUID deterministicId = idGenerator.generateV5UUID(NAMESPACE, seed); + agreement.setId(deterministicId); + log.debug("Generated UUID v5 for CustomerAgreement: {}", deterministicId); + } + return repository.save(agreement); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(UUID id) { + return repository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public List findAll() { + return repository.findAll(); + } + + @Override + @Transactional + public void deleteById(UUID id) { + repository.deleteById(id); + } + + @Override + @Transactional(readOnly = true) + public boolean existsById(UUID id) { + return repository.existsById(id); + } + + @Override + @Transactional(readOnly = true) + public long count() { + return repository.count(); + } +} +``` + +### Task 6: Flyway Migration Updates + +**6.1 Update V3__Create_additiional_Base_Tables.sql** + +Add missing Document fields to customer_agreements table: +```sql +-- Add missing Document fields +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS document_type VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS author_name VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_value VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_date_time TIMESTAMP WITH TIME ZONE; +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_reason VARCHAR(512); + +-- Add ElectronicAddress embedded fields +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email1 VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email2 VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS web VARCHAR(256); +ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS radio VARCHAR(256); + +-- Reorder columns to match XSD (PostgreSQL/MySQL - comment out for H2) +-- Document fields: type, author_name, created_date_time, last_modified_date_time, +-- revision_number, email1, email2, web, radio, subject, title, +-- status_value, status_date_time, status_reason +-- Agreement fields: sign_date, validity_interval_start, validity_interval_duration +-- CustomerAgreement fields: load_mgmt, is_pre_pay, shut_off_date_time, currency, agreement_id +``` + +### Task 7: Testing + +**7.1 Create CustomerAgreementDtoTest.java** + +Follow Phase 18 CustomerAccountDtoTest pattern: +```java +@DisplayName("CustomerAgreementDto XML Marshalling Tests") +class CustomerAgreementDtoTest { + + private DtoExportServiceImpl dtoExportService; + + @BeforeEach + void setUp() { + EspiIdGeneratorService espiIdGeneratorService = new EspiIdGeneratorService(); + dtoExportService = new DtoExportServiceImpl(null, null, espiIdGeneratorService); + } + + @Test + @DisplayName("Should export CustomerAgreement with complete Document/Agreement fields") + void shouldExportCustomerAgreementWithCompleteFields() { + // Test all 17 fields marshal correctly + // Verify field order matches customer.xsd + } + + @Test + @DisplayName("Should verify CustomerAgreement field order matches customer.xsd") + void shouldVerifyCustomerAgreementFieldOrder() { + // Assert Document fields order (lines 819-872) + // Assert Agreement fields order + // Assert CustomerAgreement fields order (lines 159-260) + } + + @Test + @DisplayName("Should use correct customer namespace") + void shouldUseCorrectCustomerNamespace() { + // Verify cust: namespace prefix usage + // Verify http://naesb.org/espi/customer namespace + } +} +``` + +**7.2 Create CustomerAgreementRepositoryTest.java** + +Follow Phase 18 CustomerAccountRepositoryTest pattern with 21+ tests: +```java +@DisplayName("CustomerAgreement Repository Tests") +class CustomerAgreementRepositoryTest extends BaseRepositoryTest { + + @Autowired + private CustomerAgreementRepository customerAgreementRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + // 7 tests: save, retrieve, update, delete, findAll, exists, count + } + + @Nested + @DisplayName("Document Field Persistence") + class DocumentFieldPersistenceTest { + // 3 tests: All Document fields, electronicAddress, docStatus + } + + @Nested + @DisplayName("Agreement Field Persistence") + class AgreementFieldPersistenceTest { + // 2 tests: signDate, validityInterval + } + + @Nested + @DisplayName("CustomerAgreement Field Persistence") + class CustomerAgreementFieldPersistenceTest { + // 3 tests: All CustomerAgreement fields, futureStatus collection, null optional fields + } + + @Nested + @DisplayName("Base Class Functionality") + class BaseClassTest { + // 5 tests: IdentifiedObject inheritance, timestamps, unique IDs, equals/hashCode, toString + } +} +``` + +**7.3 Service and Mapper Tests** +- Create CustomerAgreementServiceTest +- Create CustomerAgreementMapperTest (if needed) + +### Task 8: Code Quality (SonarQube Compliance) + +Ensure zero violations by following Phase 18 patterns: + +**8.1 Avoid Common Violations** +- ✅ No IOException throws on test methods +- ✅ Chain multiple assertions using fluent API +- ✅ Apply @AttributeOverride directly without wrapper +- ✅ Make all embeddable objects Serializable +- ✅ Use instanceof with pattern variables +- ✅ Remove unused variables +- ✅ No Thread.sleep() in tests +- ✅ No empty catch blocks +- ✅ Use hasSameHashCodeAs() for hashCode assertions +- ✅ Remove all commented-out code + +### Task 9: Commit, Push, PR + +**9.1 Git Workflow** +```bash +# Create feature branch +git checkout -b feature/schema-compliance-phase-24-customer-agreement + +# Stage all changes +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/DateTimeIntervalMapper.java # if new +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java +git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java +git add openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java +git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java + +# Commit with detailed message +git commit -m "feat: ESPI 4.0 Schema Compliance - Phase 24: CustomerAgreement Implementation" + +# Push and create PR +git push -u origin feature/schema-compliance-phase-24-customer-agreement +gh pr create --base main --title "feat: Phase 24 - CustomerAgreement ESPI 4.0 Compliance" --body "..." +``` + +**9.2 Update Issue #28** +```bash +gh issue comment 28 --body "Phase 24: CustomerAgreement implementation completed. PR #XX ready for review." +``` + +## Expected File Changes Summary + +| File | Change Type | Description | +|------|-------------|-------------| +| CustomerAgreementEntity.java | MODIFY | Add 3 Document fields, reorder all fields, fix futureStatus type, add @AttributeOverride | +| CustomerAgreementDto.java | REWRITE | **Remove ALL Atom fields**, remove relationship DTOs, add 17 XSD fields, correct propOrder | +| CustomerAgreementMapper.java | MODIFY | Add 17 field mappings, use StatusMapper/ElectronicAddressMapper | +| DateTimeIntervalMapper.java | CREATE | New mapper for DateTimeInterval ↔ DateTimeIntervalDto | +| CustomerAgreementRepository.java | CREATE | New JpaRepository interface | +| CustomerAgreementService.java | CREATE | New service interface | +| CustomerAgreementServiceImpl.java | CREATE | New service implementation with UUID v5 generation | +| V3__Create_additiional_Base_Tables.sql | MODIFY | Add 8 new columns for Document fields | +| CustomerAgreementDtoTest.java | CREATE | 3+ XML marshalling tests | +| CustomerAgreementRepositoryTest.java | CREATE | 21+ repository tests | + +**Total**: 10 files (3 modified, 7 created) + +## Success Criteria + +✅ All 609+ existing tests pass +✅ All new CustomerAgreement tests pass (24+ new tests) +✅ Zero SonarQube violations +✅ Integration tests pass (H2, MySQL, PostgreSQL) +✅ XML marshalling validates against customer.xsd +✅ Field ordering matches customer.xsd exactly +✅ All Document base class fields implemented +✅ All Agreement base class fields implemented +✅ All CustomerAgreement specific fields implemented +✅ Repository, service, and mapper layers complete +✅ CI/CD pipeline passes all checks + +## References + +- **Phase 18 Implementation**: CustomerAccount (successful pattern to follow) +- **customer.xsd**: Lines 159-260 (CustomerAgreement), 622-660 (Agreement), 819-872 (Document) +- **Issue #28**: Phase 24 task list +- **CLAUDE.md**: Project conventions and patterns + +## Notes + +1. **🚨 CRITICAL: NO ATOM FIELDS IN DTO** 🚨 + - CustomerAgreementDto must NOT contain: published, updated, id, title, links, description + - These are handled by CustomerAtomEntryDto and AtomFeedDto + - Current DTO incorrectly has these fields - they MUST be removed + - CustomerAccountDto from Phase 18 shows correct pattern (ONLY XSD fields) + +2. **Reuse Existing Mappers**: StatusMapper and ElectronicAddressMapper from Phase 18 + +3. **Serializable Requirement**: All embedded classes must implement Serializable + +4. **UUID v5 Generation**: Use namespace "ESPI-CUSTOMER-AGREEMENT" with agreementId seed + +5. **TODO Collections**: DemandResponseProgram and PricingStructures commented out - future implementation + +6. **Atom Links**: Relationships to CustomerAccount/ServiceLocation/ServiceSupplier via Atom `` links (NOT embedded DTOs) + +7. **futureStatus**: Element collection of Status objects (not CustomerEntity.Status) + +8. **Follow Phase 18 Pattern**: Use CustomerAccount implementation as template for consistency + +--- + +**Plan Created**: 2026-01-25 +**Target Completion**: Phase 24 implementation +**Next Phase**: Phase 25 - EndDevice diff --git a/PHASE_24_TASK_BREAKDOWN.md b/PHASE_24_TASK_BREAKDOWN.md new file mode 100644 index 00000000..84cff8ab --- /dev/null +++ b/PHASE_24_TASK_BREAKDOWN.md @@ -0,0 +1,1003 @@ +# Phase 24: CustomerAgreement - Task Breakdown with Dependencies + +## Task Dependency Graph + +``` +PREPARATION PHASE +├─ T1: Create Feature Branch +│ +INFRASTRUCTURE PHASE (Can run in parallel after T1) +├─ T2: Create Repository Interface (depends: T1) +├─ T3: Create Service Interface (depends: T1) +├─ T4: Create Service Implementation (depends: T2, T3) +│ +MAPPER PHASE (Can run in parallel after T1) +├─ T5: Check/Create DateTimeIntervalDto (depends: T1) +├─ T6: Check/Create DateTimeIntervalMapper (depends: T5) +│ +ENTITY PHASE (Can run in parallel after T1) +├─ T7: Update CustomerAgreementEntity (depends: T1) +│ ├─ T7a: Add missing Document fields +│ ├─ T7b: Reorder all fields per XSD +│ ├─ T7c: Fix futureStatus type +│ ├─ T7d: Add @AttributeOverride +│ └─ T7e: Update equals/hashCode +│ +DTO PHASE (Can start after T5) +├─ T8: Rewrite CustomerAgreementDto (depends: T5, T7) +│ ├─ T8a: Remove ALL Atom fields +│ ├─ T8b: Remove relationship DTOs +│ ├─ T8c: Add Document fields (9) +│ ├─ T8d: Add Agreement fields (2) +│ └─ T8e: Add CustomerAgreement fields (6) +│ +MAPPER UPDATE PHASE (Can start after T6, T8) +├─ T9: Update CustomerAgreementMapper (depends: T6, T8) +│ +DATABASE PHASE (Can run in parallel, but sync before testing) +├─ T10: Update Flyway Migration (depends: T7) +│ +TESTING PHASE (Can only start after all above complete) +├─ T11: Create CustomerAgreementDtoTest (depends: T8, T9) +├─ T12: Create CustomerAgreementRepositoryTest (depends: T2, T4, T7, T10) +├─ T13: Run All Tests (depends: T11, T12) +│ +QUALITY PHASE +├─ T14: Fix SonarQube Violations (depends: T13) +├─ T15: Run Integration Tests (depends: T14) +│ +DELIVERY PHASE +├─ T16: Commit and Push (depends: T15) +├─ T17: Create Pull Request (depends: T16) +└─ T18: Update Issue #28 (depends: T17) +``` + +--- + +## Detailed Task List + +### PREPARATION PHASE + +#### T1: Create Feature Branch +**Dependencies**: None +**Complexity**: Low +**Estimated Time**: 2 minutes + +**Commands**: +```bash +git checkout main +git pull origin main +git checkout -b feature/schema-compliance-phase-24-customer-agreement +``` + +**Verification**: +```bash +git branch --show-current # Should show: feature/schema-compliance-phase-24-customer-agreement +``` + +--- + +### INFRASTRUCTURE PHASE + +#### T2: Create Repository Interface +**Dependencies**: T1 +**Complexity**: Low +**Estimated Time**: 5 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java` + +**Actions**: +1. Create new file CustomerAgreementRepository.java +2. Extend JpaRepository +3. Add @Repository annotation +4. NO custom query methods (use only inherited methods) + +**Template**: +```java +@Repository +public interface CustomerAgreementRepository extends JpaRepository { + // Use only inherited methods to avoid H2 keyword conflicts +} +``` + +**Verification**: +- File compiles successfully +- No custom query methods defined + +--- + +#### T3: Create Service Interface +**Dependencies**: T1 +**Complexity**: Low +**Estimated Time**: 10 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java` + +**Actions**: +1. Create new file CustomerAgreementService.java +2. Define 6 essential methods: + - save(CustomerAgreementEntity) + - findById(UUID) + - findAll() + - deleteById(UUID) + - existsById(UUID) + - count() + +**Verification**: +- File compiles successfully +- Only essential CRUD methods defined + +--- + +#### T4: Create Service Implementation +**Dependencies**: T2, T3 +**Complexity**: Medium +**Estimated Time**: 20 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java` + +**Actions**: +1. Create new file CustomerAgreementServiceImpl.java +2. Add @Service, @RequiredArgsConstructor, @Slf4j annotations +3. Inject CustomerAgreementRepository +4. Inject EspiIdGeneratorService +5. Implement all 6 methods from interface +6. Add UUID v5 generation in save() method + - Namespace: "ESPI-CUSTOMER-AGREEMENT" + - Seed: agreementId or random UUID + +**Verification**: +- File compiles successfully +- All interface methods implemented +- UUID v5 generation working +- Logging statements present + +--- + +### MAPPER PHASE + +#### T5: Check/Create DateTimeIntervalDto +**Dependencies**: T1 +**Complexity**: Low-Medium +**Estimated Time**: 15 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/DateTimeIntervalDto.java` (if missing) + +**Actions**: +1. Check if DateTimeIntervalDto exists in common DTOs +2. If missing, create with fields: start (OffsetDateTime), duration (Long) +3. Add proper JAXB annotations matching usage.xsd DateTimeInterval +4. Use appropriate namespace (likely espi namespace) + +**Verification**: +- DTO compiles successfully +- Fields match usage.xsd DateTimeInterval type +- Proper JAXB annotations present + +--- + +#### T6: Check/Create DateTimeIntervalMapper +**Dependencies**: T5 +**Complexity**: Low +**Estimated Time**: 10 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/DateTimeIntervalMapper.java` (if missing) + +**Actions**: +1. Check if DateTimeIntervalMapper exists +2. If missing, create MapStruct mapper +3. Map DateTimeInterval (entity) ↔ DateTimeIntervalDto +4. Add @Mapper(componentModel = "spring") + +**Verification**: +- Mapper compiles successfully +- MapStruct generates implementation +- Bidirectional mapping works + +--- + +### ENTITY PHASE + +#### T7: Update CustomerAgreementEntity +**Dependencies**: T1 +**Complexity**: High +**Estimated Time**: 45 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java` + +**Sub-tasks**: + +##### T7a: Add Missing Document Fields (3 fields) +**Actions**: +1. Add `type` field (move from line 85 to after class declaration) +2. Add `authorName` field (NEW) +3. Add `electronicAddress` embedded field (NEW) - Organisation.ElectronicAddress +4. Add `docStatus` embedded field (NEW) - Status + +**New Code**: +```java +// Document fields - FIRST section +@Column(name = "document_type", length = 256) +private String type; // MOVED from line 85 + +@Column(name = "author_name", length = 256) +private String authorName; // NEW + +// createdDateTime (already exists) +// lastModifiedDateTime (already exists) +// revisionNumber (already exists) + +@Embedded +private Organisation.ElectronicAddress electronicAddress; // NEW + +// subject (already exists) +// title (already exists) + +@Embedded +private Status docStatus; // NEW +``` + +##### T7b: Reorder All Fields Per XSD +**Actions**: +1. Reorder to match XSD sequence: + - Document fields (9): type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus + - Agreement fields (2): signDate, validityInterval + - CustomerAgreement fields (6): loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementId + +2. Update JavaDoc comments to reflect new order + +##### T7c: Fix futureStatus Type +**Actions**: +1. Change from `List` to `List` +2. Apply @AttributeOverride annotations DIRECTLY (no wrapper): + ```java + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "customer_agreement_future_status", + joinColumns = @JoinColumn(name = "customer_agreement_id")) + @AttributeOverride(name = "value", column = @Column(name = "status_value")) + @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) + @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) + private List futureStatus; + ``` + +##### T7d: Add Class-Level @AttributeOverride +**Actions**: +1. Add @AttributeOverride annotations directly on class (no wrapper): + ```java + @Entity + @Table(name = "customer_agreements") + @AttributeOverride(name = "upLink.rel", column = @Column(name = "customer_agreement_up_link_rel")) + @AttributeOverride(name = "upLink.href", column = @Column(name = "customer_agreement_up_link_href")) + @AttributeOverride(name = "upLink.type", column = @Column(name = "customer_agreement_up_link_type")) + @AttributeOverride(name = "selfLink.rel", column = @Column(name = "customer_agreement_self_link_rel")) + @AttributeOverride(name = "selfLink.href", column = @Column(name = "customer_agreement_self_link_href")) + @AttributeOverride(name = "selfLink.type", column = @Column(name = "customer_agreement_self_link_type")) + ``` + +##### T7e: Update equals/hashCode for Pattern Matching +**Actions**: +1. Use instanceof with pattern variables: + ```java + Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? + hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ? + hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + ``` + +**Verification**: +- Entity compiles successfully +- All 17 fields present in correct order +- @AttributeOverride applied directly (no wrapper) +- equals/hashCode use pattern matching +- toString() includes all new fields + +--- + +### DTO PHASE + +#### T8: Rewrite CustomerAgreementDto +**Dependencies**: T5 (DateTimeIntervalDto), T7 (Entity updates for reference) +**Complexity**: High +**Estimated Time**: 60 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java` + +**Sub-tasks**: + +##### T8a: Remove ALL Atom Fields (10 items) +**Actions - DELETE these fields**: +```java +❌ private Long id; // Line 53 +❌ private OffsetDateTime published; // Line 59 +❌ private OffsetDateTime updated; // Line 62 +❌ private List relatedLinks; // Lines 64-66 +❌ private LinkDto selfLink; // Lines 68-69 +❌ private LinkDto upLink; // Lines 71-72 +❌ private String description; // Lines 74-75 +❌ public String getSelfHref() {...} // Lines 107-109 +❌ public String getUpHref() {...} // Lines 116-118 +❌ public String generateSelfHref() {...} // Lines 125-130 +❌ public String generateUpHref() {...} // Lines 137-142 +``` + +##### T8b: Remove Relationship DTOs (3 items) +**Actions - DELETE these fields**: +```java +❌ private CustomerAccountDto customerAccount; // Lines 83-84 +❌ private List serviceLocations; // Lines 86-88 +❌ private List statements; // Lines 90-92 +``` + +##### T8c: Add Document Fields (9 fields) +**Actions - ADD these fields in order**: +```java +// 1. type +@XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") +private String type; + +// 2. authorName +@XmlElement(name = "authorName", namespace = "http://naesb.org/espi/customer") +private String authorName; + +// 3. createdDateTime +@XmlElement(name = "createdDateTime", namespace = "http://naesb.org/espi/customer") +private OffsetDateTime createdDateTime; + +// 4. lastModifiedDateTime +@XmlElement(name = "lastModifiedDateTime", namespace = "http://naesb.org/espi/customer") +private OffsetDateTime lastModifiedDateTime; + +// 5. revisionNumber +@XmlElement(name = "revisionNumber", namespace = "http://naesb.org/espi/customer") +private String revisionNumber; + +// 6. electronicAddress +@XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") +private CustomerDto.ElectronicAddressDto electronicAddress; + +// 7. subject +@XmlElement(name = "subject", namespace = "http://naesb.org/espi/customer") +private String subject; + +// 8. title +@XmlElement(name = "title", namespace = "http://naesb.org/espi/customer") +private String title; + +// 9. docStatus +@XmlElement(name = "docStatus", namespace = "http://naesb.org/espi/customer") +private CustomerAccountDto.StatusDto docStatus; +``` + +##### T8d: Add Agreement Fields (2 fields) +**Actions - ADD these fields**: +```java +// 10. signDate (already exists, verify annotation) +@XmlElement(name = "signDate", namespace = "http://naesb.org/espi/customer") +private OffsetDateTime signDate; + +// 11. validityInterval - CHANGE TYPE from String to DateTimeIntervalDto +@XmlElement(name = "validityInterval", namespace = "http://naesb.org/espi/customer") +private DateTimeIntervalDto validityInterval; // WAS: String +``` + +##### T8e: Add CustomerAgreement Fields (6 fields) +**Actions - ADD these fields**: +```java +// 12. loadMgmt +@XmlElement(name = "loadMgmt", namespace = "http://naesb.org/espi/customer") +private String loadMgmt; + +// 13. isPrePay +@XmlElement(name = "isPrePay", namespace = "http://naesb.org/espi/customer") +private Boolean isPrePay; + +// 14. shutOffDateTime +@XmlElement(name = "shutOffDateTime", namespace = "http://naesb.org/espi/customer") +private OffsetDateTime shutOffDateTime; + +// 15. currency +@XmlElement(name = "currency", namespace = "http://naesb.org/espi/customer") +private String currency; + +// 16. futureStatus +@XmlElement(name = "Status", namespace = "http://naesb.org/espi/customer") +@XmlElementWrapper(name = "futureStatus", namespace = "http://naesb.org/espi/customer") +private List futureStatus; + +// 17. agreementId +@XmlElement(name = "agreementId", namespace = "http://naesb.org/espi/customer") +private String agreementId; +``` + +**Update @XmlType propOrder**: +```java +@XmlType(name = "CustomerAgreement", namespace = "http://naesb.org/espi/customer", propOrder = { + "type", "authorName", "createdDateTime", "lastModifiedDateTime", "revisionNumber", + "electronicAddress", "subject", "title", "docStatus", + "signDate", "validityInterval", + "loadMgmt", "isPrePay", "shutOffDateTime", "currency", "futureStatus", "agreementId" +}) +``` + +**Update Constructor(s)**: +```java +@AllArgsConstructor // Will generate constructor for all 18 fields (uuid + 17 XSD fields) +``` + +**Verification**: +- DTO compiles successfully +- NO Atom fields present +- NO relationship DTOs present +- All 17 XSD fields present in correct order +- propOrder matches field order exactly +- All fields use customer namespace + +--- + +### MAPPER UPDATE PHASE + +#### T9: Update CustomerAgreementMapper +**Dependencies**: T6 (DateTimeIntervalMapper), T8 (DTO rewrite) +**Complexity**: Medium +**Estimated Time**: 30 minutes +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java` + +**Actions**: +1. Update @Mapper uses clause: + ```java + @Mapper(componentModel = "spring", uses = { + StatusMapper.class, // From Phase 18 + ElectronicAddressMapper.class, // From Phase 18 + DateTimeIntervalMapper.class // From T6 + }) + ``` + +2. Add/Update all 17 field mappings: + ```java + @Mapping(target = "uuid", source = "id") + + // Document fields (9) + @Mapping(target = "type", source = "type") + @Mapping(target = "authorName", source = "authorName") + @Mapping(target = "createdDateTime", source = "createdDateTime") + @Mapping(target = "lastModifiedDateTime", source = "lastModifiedDateTime") + @Mapping(target = "revisionNumber", source = "revisionNumber") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "subject", source = "subject") + @Mapping(target = "title", source = "title") + @Mapping(target = "docStatus", source = "docStatus") + + // Agreement fields (2) + @Mapping(target = "signDate", source = "signDate") + @Mapping(target = "validityInterval", source = "validityInterval") + + // CustomerAgreement fields (6) + @Mapping(target = "loadMgmt", source = "loadMgmt") + @Mapping(target = "isPrePay", source = "isPrePay") + @Mapping(target = "shutOffDateTime", source = "shutOffDateTime") + @Mapping(target = "currency", source = "currency") + @Mapping(target = "futureStatus", source = "futureStatus") + @Mapping(target = "agreementId", source = "agreementId") + + CustomerAgreementDto toDto(CustomerAgreementEntity entity); + + @InheritInverseConfiguration + CustomerAgreementEntity toEntity(CustomerAgreementDto dto); + ``` + +3. Remove any old mappings for deleted fields (published, updated, links, description, etc.) + +**Verification**: +- Mapper compiles successfully +- MapStruct generates implementation without warnings +- All 17 fields mapped bidirectionally +- No unmapped target warnings for IdentifiedObject fields + +--- + +### DATABASE PHASE + +#### T10: Update Flyway Migration +**Dependencies**: T7 (Entity updates) +**Complexity**: Medium +**Estimated Time**: 20 minutes +**File**: `openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Actions**: +1. Add missing Document field columns to customer_agreements table: + ```sql + -- Add missing Document fields (3 new columns) + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS document_type VARCHAR(256); + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS author_name VARCHAR(256); + + -- Add Document.docStatus embedded fields (3 new columns) + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_value VARCHAR(256); + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_date_time TIMESTAMP WITH TIME ZONE; + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_reason VARCHAR(512); + + -- Add Document.electronicAddress embedded fields (4 new columns) + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email1 VARCHAR(256); + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email2 VARCHAR(256); + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS web VARCHAR(256); + ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS radio VARCHAR(256); + ``` + +2. Verify column order comments match new XSD order: + ```sql + -- Expected column order (matching customer.xsd): + -- Document fields: document_type, author_name, created_date_time, last_modified_date_time, + -- revision_number, email1, email2, web, radio, subject, title, + -- status_value, status_date_time, status_reason + -- Agreement fields: sign_date, validity_interval_start, validity_interval_duration + -- CustomerAgreement: load_mgmt, is_pre_pay, shut_off_date_time, currency, agreement_id + ``` + +**Verification**: +- SQL script has no syntax errors +- All 10 new columns added (3 Document + 3 Status + 4 ElectronicAddress) +- H2, MySQL, PostgreSQL compatible syntax used + +--- + +### TESTING PHASE + +#### T11: Create CustomerAgreementDtoTest +**Dependencies**: T8 (DTO rewrite), T9 (Mapper update) +**Complexity**: Medium +**Estimated Time**: 45 minutes +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java` + +**Actions**: +1. Create test class following Phase 18 CustomerAccountDtoTest pattern +2. Add @DisplayName("CustomerAgreementDto XML Marshalling Tests") +3. Setup DtoExportServiceImpl in @BeforeEach + +**Test 1: shouldExportCustomerAgreementWithCompleteFields** +```java +@Test +@DisplayName("Should export CustomerAgreement with complete Document/Agreement fields") +void shouldExportCustomerAgreementWithCompleteFields() { + // Create DTO with all 17 fields populated + // Wrap in CustomerAtomEntryDto and AtomFeedDto + // Export to XML + // Assert ALL 17 fields present in correct customer namespace + // Assert NO Atom fields inside +} +``` + +**Test 2: shouldVerifyCustomerAgreementFieldOrder** +```java +@Test +@DisplayName("Should verify CustomerAgreement field order matches customer.xsd") +void shouldVerifyCustomerAgreementFieldOrder() { + // Create DTO with all fields + // Export to XML + // Assert Document field order (9 fields) + // Assert Agreement field order (2 fields) + // Assert CustomerAgreement field order (6 fields) + // Use indexOf() to verify sequence matches XSD +} +``` + +**Test 3: shouldUseCorrectCustomerNamespace** +```java +@Test +@DisplayName("Should use correct customer namespace") +void shouldUseCorrectCustomerNamespace() { + // Create minimal DTO + // Export to XML + // Assert xmlns:cust="http://naesb.org/espi/customer" + // Assert present + // Assert NO espi: namespace for CustomerAgreement +} +``` + +**SonarQube Best Practices**: +- ✅ NO throws IOException on test methods +- ✅ Chain assertions using fluent API +- ✅ Use meaningful test data + +**Verification**: +- All 3 tests pass +- XML validates against customer.xsd structure +- Field order correct +- Customer namespace used + +--- + +#### T12: Create CustomerAgreementRepositoryTest +**Dependencies**: T2 (Repository), T4 (Service), T7 (Entity), T10 (Migration) +**Complexity**: High +**Estimated Time**: 90 minutes +**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java` + +**Actions**: +1. Create test class following Phase 18 CustomerAccountRepositoryTest pattern +2. Extend BaseRepositoryTest +3. Add @Autowired CustomerAgreementRepository +4. Create helper methods: createValidCustomerAgreement() + +**Test Structure** (21+ tests in 5 nested classes): + +##### Nested Class 1: CRUD Operations (7 tests) +```java +@Nested +@DisplayName("CRUD Operations") +class CrudOperationsTest { + @Test void shouldSaveAndRetrieveCustomerAgreementSuccessfully() + @Test void shouldUpdateCustomerAgreementSuccessfully() + @Test void shouldDeleteCustomerAgreementSuccessfully() + @Test void shouldFindAllCustomerAgreements() + @Test void shouldCheckIfCustomerAgreementExists() + @Test void shouldCountCustomerAgreementsCorrectly() + @Test void shouldHandleNullOptionalFields() +} +``` + +##### Nested Class 2: Document Field Persistence (3 tests) +```java +@Nested +@DisplayName("Document Field Persistence") +class DocumentFieldPersistenceTest { + @Test void shouldPersistAllDocumentBaseFieldsCorrectly() + @Test void shouldPersistDocumentElectronicAddressEmbeddedObject() + @Test void shouldPersistDocumentDocStatusEmbeddedObject() +} +``` + +##### Nested Class 3: Agreement Field Persistence (2 tests) +```java +@Nested +@DisplayName("Agreement Field Persistence") +class AgreementFieldPersistenceTest { + @Test void shouldPersistSignDateFieldCorrectly() + @Test void shouldPersistValidityIntervalEmbeddedObject() +} +``` + +##### Nested Class 4: CustomerAgreement Field Persistence (4 tests) +```java +@Nested +@DisplayName("CustomerAgreement Field Persistence") +class CustomerAgreementFieldPersistenceTest { + @Test void shouldPersistAllCustomerAgreementSpecificFieldsCorrectly() + @Test void shouldPersistFutureStatusCollectionCorrectly() + @Test void shouldHandleNullOptionalFieldsCorrectly() + @Test void shouldPersistCurrencyFieldCorrectly() +} +``` + +##### Nested Class 5: Base Class Functionality (5 tests) +```java +@Nested +@DisplayName("Base Class Functionality") +class BaseClassTest { + @Test void shouldInheritIdentifiedObjectFunctionality() + @Test void shouldUpdateTimestampsOnModification() + @Test void shouldGenerateUniqueIdsForDifferentEntities() + @Test void shouldHandleEqualsAndHashCodeCorrectly() + @Test void shouldGenerateMeaningfulToStringRepresentation() +} +``` + +**SonarQube Best Practices**: +- ✅ NO Thread.sleep() - remove from timestamp tests +- ✅ NO empty catch blocks +- ✅ Chain assertions using fluent API +- ✅ Use hasSameHashCodeAs() for hashCode tests +- ✅ NO unused variables + +**Verification**: +- All 21+ tests pass +- Tests run on H2 in-memory database +- All CRUD operations work +- All fields persist correctly +- Embedded objects work +- Collections persist + +--- + +#### T13: Run All Tests +**Dependencies**: T11 (DTO tests), T12 (Repository tests) +**Complexity**: Low +**Estimated Time**: 5 minutes + +**Actions**: +```bash +# Run only CustomerAgreement tests +mvn test -pl openespi-common -Dtest=CustomerAgreementDtoTest,CustomerAgreementRepositoryTest + +# Run all tests in openespi-common +mvn test -pl openespi-common + +# Run all tests in entire project +mvn test +``` + +**Verification**: +- All 609+ existing tests pass +- All 24+ new CustomerAgreement tests pass +- No test failures +- No compilation errors + +--- + +### QUALITY PHASE + +#### T14: Fix SonarQube Violations +**Dependencies**: T13 (All tests passing) +**Complexity**: Medium +**Estimated Time**: 30 minutes + +**Actions**: +1. Run SonarQube analysis (if available) or manual code review +2. Check for common violations: + - ❌ Unnecessary IOException throws + - ❌ Multiple separate assertions (should chain) + - ❌ @AttributeOverrides wrapper (should apply directly) + - ❌ Non-serializable embedded objects + - ❌ Old instanceof patterns (should use pattern matching) + - ❌ Unused variables + - ❌ Thread.sleep() usage + - ❌ Empty catch blocks + - ❌ Commented-out code + - ❌ Manual hashCode comparisons (should use hasSameHashCodeAs) + +3. Fix any violations found +4. Re-run tests after each fix + +**Verification**: +- Zero SonarQube violations +- All tests still pass +- Code follows Phase 18 quality standards + +--- + +#### T15: Run Integration Tests +**Dependencies**: T14 (SonarQube clean) +**Complexity**: Medium +**Estimated Time**: 10 minutes + +**Actions**: +```bash +# Run integration tests with TestContainers +mvn verify -pl openespi-common + +# This will test against: +# - H2 (in-memory) +# - MySQL 9.5 (TestContainer) +# - PostgreSQL 18 (TestContainer) +``` + +**Verification**: +- All integration tests pass on H2 +- All integration tests pass on MySQL 9.5 +- All integration tests pass on PostgreSQL 18 +- TestContainers start and stop cleanly +- No database-specific errors + +--- + +### DELIVERY PHASE + +#### T16: Commit and Push +**Dependencies**: T15 (Integration tests pass) +**Complexity**: Low +**Estimated Time**: 10 minutes + +**Actions**: +1. Stage all modified and new files: + ```bash + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/DateTimeIntervalDto.java # if new + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/DateTimeIntervalMapper.java # if new + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java + git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java + git add openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql + git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java + git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java + ``` + +2. Commit with detailed message: + ```bash + git commit -m "feat: ESPI 4.0 Schema Compliance - Phase 24: CustomerAgreement Implementation + + Implement complete CustomerAgreement ESPI 4.0 schema compliance with Document and + Agreement base class fields, embedded objects, and comprehensive testing. + + + " + ``` + +3. Push to remote: + ```bash + git push -u origin feature/schema-compliance-phase-24-customer-agreement + ``` + +**Verification**: +- All files committed +- Commit message follows convention +- Branch pushed to remote +- No uncommitted changes remain + +--- + +#### T17: Create Pull Request +**Dependencies**: T16 (Code pushed) +**Complexity**: Low +**Estimated Time**: 15 minutes + +**Actions**: +```bash +gh pr create \ + --base main \ + --head feature/schema-compliance-phase-24-customer-agreement \ + --title "feat: ESPI 4.0 Schema Compliance - Phase 24: CustomerAgreement Implementation" \ + --body "$(cat <<'EOF' +## Summary + +Implement complete CustomerAgreement ESPI 4.0 schema compliance... + + +- Key achievements +- Changes overview +- SonarQube fixes +- Test results +- ESPI 4.0 compliance +- Breaking changes (if any) +- Checklist +EOF +)" +``` + +**Verification**: +- PR created successfully +- PR description complete +- Base branch is main +- CI/CD checks start automatically + +--- + +#### T18: Update Issue #28 +**Dependencies**: T17 (PR created) +**Complexity**: Low +**Estimated Time**: 5 minutes + +**Actions**: +```bash +gh issue comment 28 --body "$(cat <<'EOF' +## Phase 24: CustomerAgreement Implementation - ✅ COMPLETED + +**Pull Request:** #XX +**Branch:** feature/schema-compliance-phase-24-customer-agreement +**Status:** Ready for review + +### Summary +Completed full ESPI 4.0 schema compliance implementation for CustomerAgreement... + +### Implementation Details +
+ +### Test Results +- ✅ All 633+ tests passing (24 new CustomerAgreement tests) +- ✅ Zero SonarQube violations +- ✅ Integration tests pass (H2, MySQL, PostgreSQL) + +EOF +)" +``` + +**Verification**: +- Comment posted to Issue #28 +- Issue remains OPEN (not closed) +- PR link included + +--- + +## Task Execution Strategy + +### Recommended Execution Order + +**Phase 1: Quick Wins** (Parallel - 30 minutes total) +- Start all infrastructure tasks: T2, T3, T4 (can work in parallel) +- Start mapper phase: T5, T6 (can work in parallel) + +**Phase 2: Core Implementation** (Sequential - 90 minutes total) +- T7: Update Entity (must complete before T8) +- T8: Rewrite DTO (needs T5, T7) +- T9: Update Mapper (needs T6, T8) +- T10: Update Migration (needs T7, can parallel with T8/T9) + +**Phase 3: Testing** (Sequential - 140 minutes total) +- T11: Create DTO Test (needs T8, T9) +- T12: Create Repository Test (needs T2, T4, T7, T10) +- T13: Run All Tests (needs T11, T12) + +**Phase 4: Quality & Delivery** (Sequential - 70 minutes total) +- T14: Fix SonarQube (needs T13) +- T15: Integration Tests (needs T14) +- T16: Commit & Push (needs T15) +- T17: Create PR (needs T16) +- T18: Update Issue (needs T17) + +### Total Estimated Time +- **Minimum**: 5.5 hours (if everything goes perfectly) +- **Expected**: 7-8 hours (with normal debugging/fixes) +- **Maximum**: 10-12 hours (if major issues encountered) + +### Critical Path +``` +T1 → T7 → T8 → T9 → T11 → T12 → T13 → T14 → T15 → T16 → T17 → T18 +``` + +### Parallelization Opportunities +- T2, T3, T5 can run in parallel after T1 +- T6 can start immediately after T5 +- T4 can start after T2 and T3 complete +- T10 can run in parallel with T8/T9 +- All tests (T11, T12) need sequential execution + +--- + +## Risk Areas + +### High Risk +- **T7**: Entity reordering - complex, many fields, easy to make mistakes +- **T8**: DTO rewrite - must remove ALL Atom fields correctly +- **T12**: Repository tests - 21+ tests, lots of edge cases + +### Medium Risk +- **T9**: Mapper updates - MapStruct unmapped field warnings +- **T10**: Migration - SQL syntax differences between databases +- **T14**: SonarQube - may discover unexpected violations + +### Low Risk +- **T2, T3, T4**: Infrastructure - straightforward CRUD +- **T5, T6**: DateTimeInterval - may already exist +- **T16-T18**: Delivery - mechanical steps + +--- + +## Success Criteria Checklist + +- [ ] T1: Feature branch created +- [ ] T2: Repository interface created +- [ ] T3: Service interface created +- [ ] T4: Service implementation created with UUID v5 +- [ ] T5: DateTimeIntervalDto exists/created +- [ ] T6: DateTimeIntervalMapper exists/created +- [ ] T7a-e: Entity updated with all fields in correct order +- [ ] T8a-e: DTO rewritten with ONLY XSD fields (NO Atom fields) +- [ ] T9: Mapper updated with all 17 field mappings +- [ ] T10: Migration adds 10 new columns +- [ ] T11: 3 DTO tests created and passing +- [ ] T12: 21+ repository tests created and passing +- [ ] T13: All 633+ tests passing +- [ ] T14: Zero SonarQube violations +- [ ] T15: Integration tests pass on all databases +- [ ] T16: Code committed and pushed +- [ ] T17: Pull request created +- [ ] T18: Issue #28 updated + +--- + +## Quick Reference: Files Modified/Created + +### Modified (3 files) +1. `CustomerAgreementEntity.java` - Add fields, reorder, fix types +2. `CustomerAgreementDto.java` - Complete rewrite, remove Atom fields +3. `V3__Create_additiional_Base_Tables.sql` - Add 10 columns + +### Created (7-9 files) +4. `CustomerAgreementRepository.java` - NEW +5. `CustomerAgreementService.java` - NEW +6. `CustomerAgreementServiceImpl.java` - NEW +7. `DateTimeIntervalDto.java` - NEW (if not exists) +8. `DateTimeIntervalMapper.java` - NEW (if not exists) +9. `CustomerAgreementDtoTest.java` - NEW +10. `CustomerAgreementRepositoryTest.java` - NEW +11. `CustomerAgreementMapper.java` - MODIFIED (update mappings) + +### Total: 10-12 files + +--- + +**Plan Created**: 2026-01-25 +**Ready for Execution**: Yes +**Estimated Completion**: 7-8 hours of focused work diff --git a/customer-dto-current-output.xml b/customer-dto-current-output.xml new file mode 100644 index 00000000..629e286b --- /dev/null +++ b/customer-dto-current-output.xml @@ -0,0 +1,65 @@ + + + + urn:uuid:feed-id + Customer Feed + 2026-01-18T00:23:31Z + 2026-01-18T00:23:31Z + + urn:uuid:550e8400-e29b-51d4-a716-446655440000 + ACME Energy Services Customer + 2026-01-18T00:23:31Z + 2026-01-18T00:23:31Z + + + + + 123 Main Street + Springfield + IL + 62701 + USA + + + PO Box 456 + Springfield + IL + 62702 + USA + + + 217 + 555-1234 + + + 217 + 555-5678 + 101 + + + customer@example.com + support@example.com + https://www.example.com + + ACME Energy Services + + RESIDENTIAL + Life support required + true + PUC-12345 + + ACTIVE + 2025-01-15T10:30:00Z + Account in good standing + + + 5 + 1 + STANDARD + + en_US + John Smith + + + + diff --git a/intervalblock-dto-output.xml b/intervalblock-dto-output.xml new file mode 100644 index 00000000..45cb9617 --- /dev/null +++ b/intervalblock-dto-output.xml @@ -0,0 +1,62 @@ + + + + urn:uuid:test-feed + Test Feed + 2026-01-18T12:17:12Z + 2026-01-18T12:17:12Z + + urn:uuid:FE9A61BB-6913-52D4-88BE-9634A218EF53 + Interval Block + 2026-01-18T12:17:12Z + 2026-01-18T12:17:12Z + + + + + + 1330578000 + 86400 + + + 974 + + 8 + + + 1330578000 + 900 + + 282 + + + 965 + + 7 + + + 1330578900 + 900 + + 323 + + + 884 + + 1330579800 + 900 + + 294 + + + 995 + + 1330580700 + 900 + + 331 + + + + + 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 3d08488f..7564c2d2 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 @@ -89,7 +89,75 @@ public static class StreetAddress implements Serializable { private String country; } - // PhoneNumber embeddable class removed - using separate PhoneNumberEntity table instead + /** + * Embeddable class for TelephoneNumber. + * Per customer.xsd TelephoneNumber type (lines 1428-1478). + * 8 fields per ESPI 4.0 specification. + */ + @Embeddable + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TelephoneNumber implements Serializable { + @Column(name = "country_code", length = 256) + private String countryCode; + + @Column(name = "area_code", length = 256) + private String areaCode; + + @Column(name = "city_code", length = 256) + private String cityCode; + + @Column(name = "local_number", length = 256) + private String localNumber; + + @Column(name = "ext", length = 256) + private String ext; + + @Column(name = "dial_out", length = 256) + private String dialOut; + + @Column(name = "international_prefix", length = 256) + private String internationalPrefix; + + @Column(name = "itu_phone", length = 256) + private String ituPhone; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TelephoneNumber that = (TelephoneNumber) o; + return java.util.Objects.equals(countryCode, that.countryCode) && + java.util.Objects.equals(areaCode, that.areaCode) && + java.util.Objects.equals(cityCode, that.cityCode) && + java.util.Objects.equals(localNumber, that.localNumber) && + java.util.Objects.equals(ext, that.ext) && + java.util.Objects.equals(dialOut, that.dialOut) && + java.util.Objects.equals(internationalPrefix, that.internationalPrefix) && + java.util.Objects.equals(ituPhone, that.ituPhone); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone); + } + + @Override + public String toString() { + return "TelephoneNumber{" + + "countryCode='" + countryCode + '\'' + + ", areaCode='" + areaCode + '\'' + + ", cityCode='" + cityCode + '\'' + + ", localNumber='" + localNumber + '\'' + + ", ext='" + ext + '\'' + + ", dialOut='" + dialOut + '\'' + + ", internationalPrefix='" + internationalPrefix + '\'' + + ", ituPhone='" + ituPhone + '\'' + + '}'; + } + } /** * Embeddable class for ElectronicAddress. 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 index 696199f8..a4ce5b6c 100644 --- 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 @@ -56,29 +56,53 @@ public class PhoneNumberEntity { private UUID id; /** - * Area code for phone number. + * Country code (per customer.xsd TelephoneNumber). */ - @Column(name = "area_code", length = 10) + @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 for phone number. + * City code (per customer.xsd TelephoneNumber). */ - @Column(name = "city_code", length = 10) + @Column(name = "city_code", length = 256) private String cityCode; /** - * Local number for phone number. + * Main (local) part of this telephone number (per customer.xsd TelephoneNumber). */ - @Column(name = "local_number", length = 20) + @Column(name = "local_number", length = 256) private String localNumber; /** - * Extension for phone number. + * Extension for this telephone number (per customer.xsd TelephoneNumber "ext" element). */ - @Column(name = "extension", length = 10) + @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.). */ diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java index 096a1ac1..ba7f4a19 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java @@ -23,9 +23,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.ToString; import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; -import org.hibernate.annotations.SQLRestriction; import org.hibernate.proxy.HibernateProxy; import java.util.List; @@ -43,7 +41,8 @@ @Getter @Setter @NoArgsConstructor -public class ServiceLocationEntity extends IdentifiedObject { +public class +ServiceLocationEntity extends IdentifiedObject { // Location fields (previously inherited from Location superclass) @@ -58,52 +57,66 @@ public class ServiceLocationEntity extends IdentifiedObject { * Main address of the location. */ @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "streetDetail", column = @Column(name = "main_street_detail")), - @AttributeOverride(name = "townDetail", column = @Column(name = "main_town_detail")), - @AttributeOverride(name = "stateOrProvince", column = @Column(name = "main_state_or_province")), - @AttributeOverride(name = "postalCode", column = @Column(name = "main_postal_code")), - @AttributeOverride(name = "country", column = @Column(name = "main_country")) - }) + @AttributeOverride(name = "streetDetail", column = @Column(name = "main_street_detail")) + @AttributeOverride(name = "townDetail", column = @Column(name = "main_town_detail")) + @AttributeOverride(name = "stateOrProvince", column = @Column(name = "main_state_or_province")) + @AttributeOverride(name = "postalCode", column = @Column(name = "main_postal_code")) + @AttributeOverride(name = "country", column = @Column(name = "main_country")) private Organisation.StreetAddress mainAddress; /** * Secondary address of the location. For example, PO Box address may have different ZIP code than that in the 'mainAddress'. */ @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "streetDetail", column = @Column(name = "secondary_street_detail")), - @AttributeOverride(name = "townDetail", column = @Column(name = "secondary_town_detail")), - @AttributeOverride(name = "stateOrProvince", column = @Column(name = "secondary_state_or_province")), - @AttributeOverride(name = "postalCode", column = @Column(name = "secondary_postal_code")), - @AttributeOverride(name = "country", column = @Column(name = "secondary_country")) - }) + @AttributeOverride(name = "streetDetail", column = @Column(name = "secondary_street_detail")) + @AttributeOverride(name = "townDetail", column = @Column(name = "secondary_town_detail")) + @AttributeOverride(name = "stateOrProvince", column = @Column(name = "secondary_state_or_province")) + @AttributeOverride(name = "postalCode", column = @Column(name = "secondary_postal_code")) + @AttributeOverride(name = "country", column = @Column(name = "secondary_country")) private Organisation.StreetAddress secondaryAddress; /** - * Phone numbers associated with this service location. - * Uses separate PhoneNumberEntity table to avoid JPA mapping conflicts. + * Primary phone number for this service location. + * Per customer.xsd Location.phone1 (TelephoneNumber type, lines 1428-1478). */ - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) - @JoinColumn(name = "parent_entity_uuid", referencedColumnName = "id") - @SQLRestriction("parent_entity_type = 'ServiceLocationEntity'") - @ToString.Exclude - private List phoneNumbers; + @Embedded + @AttributeOverride(name = "countryCode", column = @Column(name = "phone1_country_code")) + @AttributeOverride(name = "areaCode", column = @Column(name = "phone1_area_code")) + @AttributeOverride(name = "cityCode", column = @Column(name = "phone1_city_code")) + @AttributeOverride(name = "localNumber", column = @Column(name = "phone1_local_number")) + @AttributeOverride(name = "ext", column = @Column(name = "phone1_ext")) + @AttributeOverride(name = "dialOut", column = @Column(name = "phone1_dial_out")) + @AttributeOverride(name = "internationalPrefix", column = @Column(name = "phone1_international_prefix")) + @AttributeOverride(name = "ituPhone", column = @Column(name = "phone1_itu_phone")) + private Organisation.TelephoneNumber phone1; + + /** + * Secondary phone number for this service location. + * Per customer.xsd Location.phone2 (TelephoneNumber type, lines 1428-1478). + */ + @Embedded + @AttributeOverride(name = "countryCode", column = @Column(name = "phone2_country_code")) + @AttributeOverride(name = "areaCode", column = @Column(name = "phone2_area_code")) + @AttributeOverride(name = "cityCode", column = @Column(name = "phone2_city_code")) + @AttributeOverride(name = "localNumber", column = @Column(name = "phone2_local_number")) + @AttributeOverride(name = "ext", column = @Column(name = "phone2_ext")) + @AttributeOverride(name = "dialOut", column = @Column(name = "phone2_dial_out")) + @AttributeOverride(name = "internationalPrefix", column = @Column(name = "phone2_international_prefix")) + @AttributeOverride(name = "ituPhone", column = @Column(name = "phone2_itu_phone")) + private Organisation.TelephoneNumber phone2; /** * Electronic address. */ @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "lan", column = @Column(name = "electronic_lan")), - @AttributeOverride(name = "mac", column = @Column(name = "electronic_mac")), - @AttributeOverride(name = "email1", column = @Column(name = "electronic_email1")), - @AttributeOverride(name = "email2", column = @Column(name = "electronic_email2")), - @AttributeOverride(name = "web", column = @Column(name = "electronic_web")), - @AttributeOverride(name = "radio", column = @Column(name = "electronic_radio")), - @AttributeOverride(name = "userID", column = @Column(name = "electronic_user_id")), - @AttributeOverride(name = "password", column = @Column(name = "electronic_password")) - }) + @AttributeOverride(name = "lan", column = @Column(name = "electronic_lan")) + @AttributeOverride(name = "mac", column = @Column(name = "electronic_mac")) + @AttributeOverride(name = "email1", column = @Column(name = "electronic_email1")) + @AttributeOverride(name = "email2", column = @Column(name = "electronic_email2")) + @AttributeOverride(name = "web", column = @Column(name = "electronic_web")) + @AttributeOverride(name = "radio", column = @Column(name = "electronic_radio")) + @AttributeOverride(name = "userID", column = @Column(name = "electronic_user_id")) + @AttributeOverride(name = "password", column = @Column(name = "electronic_password")) private Organisation.ElectronicAddress electronicAddress; /** @@ -122,12 +135,11 @@ public class ServiceLocationEntity extends IdentifiedObject { * Status of this location. */ @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "value", column = @Column(name = "status_value")), - @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")), - @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) - }) - private CustomerEntity.Status status; + @AttributeOverride(name = "value", column = @Column(name = "status_value")) + @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) + @AttributeOverride(name = "remark", column = @Column(name = "status_remark")) + @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) + private Status status; // WorkLocation fields (WorkLocation is simply a Location specialized for work activities - no additional fields) @@ -148,18 +160,31 @@ public class ServiceLocationEntity extends IdentifiedObject { private String siteAccessProblem; /** - * True if inspection is needed of facilities at this service location. This could be requested by a customer, + * True if inspection is needed of facilities at this service location. This could be requested by a customer, * due to suspected tampering, environmental concerns (e.g., a fire in the vicinity), or to correct incompatible data. */ @Column(name = "needs_inspection") private Boolean needsInspection; /** - * All usage points delivering service (of the same type) to this service location. - * TODO: Create UsagePointsEntity and enable this relationship + * Collection of UsagePoint resource href URLs (cross-stream reference from customer.xsd to usage.xsd). + * Stores the Atom self-link href URLs for UsagePoint resources, NOT Atom link elements. + * Each string is the UsagePoint's atom:link[@rel='self']/@href value. + * + * Per ESPI 4.0 customer.xsd lines 1106-1111: UsagePoints element contains sequence of UsagePoint elements, + * where each UsagePoint is of type xs:anyURI. + * + * Example: ["https://api.example.com/espi/1_1/resource/UsagePoint/12345", + * "https://api.example.com/espi/1_1/resource/UsagePoint/67890"] + * + * ServiceLocation exists in PII data stream, UsagePoint exists in non-PII data stream. + * This collection provides cross-stream references without violating data separation. */ - // @OneToMany(mappedBy = "serviceLocation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - // private List usagePoints; + @ElementCollection + @CollectionTable(name = "service_location_usage_point_hrefs", + joinColumns = @JoinColumn(name = "service_location_id")) + @Column(name = "usage_point_href", length = 512) + private List usagePointHrefs; /** * [extension] Outage Block Identifier @@ -215,6 +240,7 @@ public String toString() { "accessMethod = " + getAccessMethod() + ", " + "siteAccessProblem = " + getSiteAccessProblem() + ", " + "needsInspection = " + getNeedsInspection() + ", " + + "usagePointHrefs = " + getUsagePointHrefs() + ", " + "outageBlock = " + getOutageBlock() + ", " + "description = " + getDescription() + ", " + "created = " + getCreated() + ", " + 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 e3e8ed73..6286ab2c 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 @@ -27,6 +27,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; import java.time.OffsetDateTime; /** @@ -79,23 +80,6 @@ public class CustomerDto { /** * Embeddable DTO for Status. */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "Status", namespace = "http://naesb.org/espi/customer") - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class StatusDto { - @XmlElement(name = "value", namespace = "http://naesb.org/espi/customer") - private String value; - - @XmlElement(name = "dateTime", namespace = "http://naesb.org/espi/customer") - private OffsetDateTime dateTime; - - @XmlElement(name = "reason", namespace = "http://naesb.org/espi/customer") - private String reason; - } - /** * Embeddable DTO for Priority. */ @@ -136,10 +120,10 @@ public static class OrganisationDto { private StreetAddressDto postalAddress; @XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") - private PhoneNumberDto phone1; + private TelephoneNumberDto phone1; @XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") - private PhoneNumberDto phone2; + private TelephoneNumberDto phone2; @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") private ElectronicAddressDto electronicAddress; @@ -177,13 +161,23 @@ public static class StreetAddressDto { /** * 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 = "PhoneNumber", namespace = "http://naesb.org/espi/customer") + @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 PhoneNumberDto { + 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; @@ -193,8 +187,17 @@ public static class PhoneNumberDto { @XmlElement(name = "localNumber", namespace = "http://naesb.org/espi/customer") private String localNumber; - @XmlElement(name = "extension", namespace = "http://naesb.org/espi/customer") - private String extension; + @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; } /** 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 d419e74f..5e17674a 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 @@ -19,14 +19,13 @@ package org.greenbuttonalliance.espi.common.dto.customer; -import org.greenbuttonalliance.espi.common.dto.atom.LinkDto; - 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; import java.util.List; @@ -34,114 +33,156 @@ * ServiceLocation DTO class for JAXB XML marshalling/unmarshalling. * * Represents a physical location where utility services are delivered. - * Supports Atom protocol XML wrapping. + * Per ESPI 4.0 customer.xsd: ServiceLocation extends WorkLocation extends Location. */ @XmlRootElement(name = "ServiceLocation", namespace = "http://naesb.org/espi/customer") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "ServiceLocation", namespace = "http://naesb.org/espi/customer", propOrder = { - "published", "updated", "selfLink", "upLink", "relatedLinks", - "description", "accessMethod", "needsInspection", "siteAccessProblem", - "positionAddress", "geoInfoReference", "direction", "customerAgreement" + // Location fields (customer.xsd lines 914-997) + "type", "mainAddress", "secondaryAddress", "phone1", "phone2", + "electronicAddress", "geoInfoReference", "direction", "status", "positionPoints", + // ServiceLocation fields (customer.xsd lines 1074-1116) + "accessMethod", "siteAccessProblem", "needsInspection", "usagePointHrefs", "outageBlock" }) @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class ServiceLocationDto { +public class ServiceLocationDto implements Serializable { @XmlTransient - private Long id; + private String id; @XmlAttribute(name = "mRID") private String uuid; - @XmlElement(name = "published") - private OffsetDateTime published; + // Location fields (inherited from Location → WorkLocation → ServiceLocation) - @XmlElement(name = "updated") - private OffsetDateTime updated; + /** + * Classification by utility's corporate standards and practices, relative to the location itself. + */ + @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") + private String type; - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - @XmlElementWrapper(name = "links", namespace = "http://www.w3.org/2005/Atom") - private List relatedLinks; + /** + * Main address of the location. + */ + @XmlElement(name = "mainAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.StreetAddressDto mainAddress; - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto selfLink; + /** + * Secondary address of the location (e.g., PO Box with different ZIP code). + */ + @XmlElement(name = "secondaryAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.StreetAddressDto secondaryAddress; - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto upLink; + /** + * Primary phone number for this service location. + */ + @XmlElement(name = "phone1", namespace = "http://naesb.org/espi/customer") + private CustomerDto.TelephoneNumberDto phone1; - @XmlElement(name = "description") - private String description; + /** + * Secondary phone number for this service location. + */ + @XmlElement(name = "phone2", namespace = "http://naesb.org/espi/customer") + private CustomerDto.TelephoneNumberDto phone2; - @XmlElement(name = "accessMethod") - private String accessMethod; + /** + * Electronic address (email, web, etc.). + */ + @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") + private CustomerDto.ElectronicAddressDto electronicAddress; - @XmlElement(name = "needsInspection") - private Boolean needsInspection; + /** + * Reference to geographical information source, often external to the utility. + */ + @XmlElement(name = "geoInfoReference", namespace = "http://naesb.org/espi/customer") + private String geoInfoReference; - @XmlElement(name = "siteAccessProblem") - private String siteAccessProblem; + /** + * Direction that allows field crews to quickly find a given asset. + */ + @XmlElement(name = "direction", namespace = "http://naesb.org/espi/customer") + private String direction; - @XmlElement(name = "positionAddress") - private String positionAddress; + /** + * Status of this location. + */ + @XmlElement(name = "status", namespace = "http://naesb.org/espi/customer") + private StatusDto status; - @XmlElement(name = "geoInfoReference") - private String geoInfoReference; + /** + * Sequence of position points describing this location. + * Each point contains xPosition, yPosition, and optional zPosition coordinates. + */ + @XmlElement(name = "positionPoints", namespace = "http://naesb.org/espi/customer") + private List positionPoints; - @XmlElement(name = "direction") - private String direction; + // ServiceLocation specific fields - @XmlElement(name = "CustomerAgreement") - private CustomerAgreementDto customerAgreement; + /** + * Method for the service person to access this service location. + */ + @XmlElement(name = "accessMethod", namespace = "http://naesb.org/espi/customer") + private String accessMethod; /** - * Minimal constructor for basic location data. + * Problems previously encountered when visiting or performing work on this location. */ - public ServiceLocationDto(String uuid, String positionAddress) { - this(null, uuid, null, null, null, null, null, null, - null, null, null, positionAddress, null, null, null); - } + @XmlElement(name = "siteAccessProblem", namespace = "http://naesb.org/espi/customer") + private String siteAccessProblem; /** - * Gets the self href for this service location. - * - * @return self href string + * True if inspection is needed of facilities at this service location. */ - public String getSelfHref() { - return selfLink != null ? selfLink.getHref() : null; - } + @XmlElement(name = "needsInspection", namespace = "http://naesb.org/espi/customer") + private Boolean needsInspection; /** - * Gets the up href for this service location. - * - * @return up href string + * Collection of UsagePoint resource href URLs (cross-stream reference). + * Each string is the UsagePoint's atom:link[@rel='self']/@href value. + * Per ESPI 4.0 customer.xsd lines 1106-1111. */ - public String getUpHref() { - return upLink != null ? upLink.getHref() : null; - } + @XmlElement(name = "UsagePoints", namespace = "http://naesb.org/espi/customer") + private List usagePointHrefs; /** - * Generates the default self href for a service location. - * - * @return default self href + * Outage Block Identifier (extension). */ - public String generateSelfHref() { - if (uuid != null && customerAgreement != null && customerAgreement.getUuid() != null) { - return "/espi/1_1/resource/CustomerAgreement/" + customerAgreement.getUuid() + "/ServiceLocation/" + uuid; - } - return uuid != null ? "/espi/1_1/resource/ServiceLocation/" + uuid : null; - } + @XmlElement(name = "outageBlock", namespace = "http://naesb.org/espi/customer") + private String outageBlock; /** - * Generates the default up href for a service location. - * - * @return default up href + * PositionPoint DTO nested class. + * Per customer.xsd PositionPoint type (lines 1146-1180). + * Spatial coordinates for a point in the coordinate system. */ - public String generateUpHref() { - if (customerAgreement != null && customerAgreement.getUuid() != null) { - return "/espi/1_1/resource/CustomerAgreement/" + customerAgreement.getUuid() + "/ServiceLocation"; - } - return "/espi/1_1/resource/ServiceLocation"; + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "PositionPoint", namespace = "http://naesb.org/espi/customer", propOrder = { + "xPosition", "yPosition", "zPosition" + }) + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class PositionPointDto implements Serializable { + /** + * X axis position. + */ + @XmlElement(name = "xPosition", namespace = "http://naesb.org/espi/customer") + private String xPosition; + + /** + * Y axis position. + */ + @XmlElement(name = "yPosition", namespace = "http://naesb.org/espi/customer") + private String yPosition; + + /** + * Z axis position (if applicable). + */ + @XmlElement(name = "zPosition", namespace = "http://naesb.org/espi/customer") + private String zPosition; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatusDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatusDto.java new file mode 100644 index 00000000..9324e9fe --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatusDto.java @@ -0,0 +1,67 @@ +/* + * + * 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.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +/** + * Shared DTO representing Status information for customer entities. + * Per ESPI 4.0 customer.xsd lines 1254-1280. + *

+ * Status contains current status information relevant to an entity with 4 fields: + * - value: Status value at dateTime + * - dateTime: Date and time for which status applies + * - remark: Pertinent information as free form text + * - reason: Reason code or explanation for the status + *

+ * This is a shared type used by Customer, ServiceLocation, and other customer entities. + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Status", namespace = "http://naesb.org/espi/customer", propOrder = { + "value", "dateTime", "remark", "reason" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class StatusDto implements Serializable { + + @XmlElement(name = "value", namespace = "http://naesb.org/espi/customer") + private String value; + + @XmlElement(name = "dateTime", namespace = "http://naesb.org/espi/customer") + private OffsetDateTime dateTime; + + @XmlElement(name = "remark", namespace = "http://naesb.org/espi/customer") + private String remark; + + @XmlElement(name = "reason", namespace = "http://naesb.org/espi/customer") + private String reason; +} 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 3608c7c4..bb9170f3 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 @@ -102,8 +102,8 @@ default CustomerDto.OrganisationDto mapOrganisation(CustomerEntity entity) { List phoneNumbers = entity.getPhoneNumbers(); // Extract phone numbers by type - CustomerDto.PhoneNumberDto phone1 = extractPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.PRIMARY); - CustomerDto.PhoneNumberDto phone2 = extractPhoneByType(phoneNumbers, PhoneNumberEntity.PhoneType.SECONDARY); + 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( @@ -188,17 +188,22 @@ default Organisation.ElectronicAddress mapElectronicAddressFromDto(CustomerDto.E } // Helper method to extract phone number by type - default CustomerDto.PhoneNumberDto extractPhoneByType(List phoneNumbers, PhoneNumberEntity.PhoneType 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.PhoneNumberDto( - phone.getAreaCode(), - phone.getCityCode(), - phone.getLocalNumber(), - phone.getExtension() + .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); } 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 new file mode 100644 index 00000000..51ea43a7 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java @@ -0,0 +1,246 @@ +/* + * + * 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.Organisation; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; +import org.greenbuttonalliance.espi.common.dto.customer.ServiceLocationDto; +import org.greenbuttonalliance.espi.common.dto.customer.StatusDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * MapStruct mapper for converting between ServiceLocationEntity and ServiceLocationDto. + * + * Maps only ServiceLocation 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 +}) +public interface ServiceLocationMapper { + + /** + * Converts a ServiceLocationEntity to a ServiceLocationDto. + * Maps all Location and ServiceLocation fields including embedded objects. + * + * @param entity the service location entity + * @return the service location DTO + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "uuid", source = "id") + @Mapping(target = "type", source = "type") + @Mapping(target = "mainAddress", source = "mainAddress") + @Mapping(target = "secondaryAddress", source = "secondaryAddress") + @Mapping(target = "phone1", source = "phone1") + @Mapping(target = "phone2", source = "phone2") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "geoInfoReference", source = "geoInfoReference") + @Mapping(target = "direction", source = "direction") + @Mapping(target = "status", source = "status") + @Mapping(target = "positionPoints", expression = "java(null)") // TODO: Implement when PositionPointEntity is created + @Mapping(target = "accessMethod", source = "accessMethod") + @Mapping(target = "siteAccessProblem", source = "siteAccessProblem") + @Mapping(target = "needsInspection", source = "needsInspection") + @Mapping(target = "usagePointHrefs", source = "usagePointHrefs") + @Mapping(target = "outageBlock", source = "outageBlock") + ServiceLocationDto toDto(ServiceLocationEntity entity); + + /** + * Converts a ServiceLocationDto to a ServiceLocationEntity. + * Maps all Location and ServiceLocation fields including embedded objects. + * + * @param dto the service location DTO + * @return the service location entity + */ + @Mapping(target = "id", source = "uuid") + @Mapping(target = "type", source = "type") + @Mapping(target = "mainAddress", source = "mainAddress") + @Mapping(target = "secondaryAddress", source = "secondaryAddress") + @Mapping(target = "phone1", source = "phone1") + @Mapping(target = "phone2", source = "phone2") + @Mapping(target = "electronicAddress", source = "electronicAddress") + @Mapping(target = "geoInfoReference", source = "geoInfoReference") + @Mapping(target = "direction", source = "direction") + @Mapping(target = "status", source = "status") + @Mapping(target = "accessMethod", source = "accessMethod") + @Mapping(target = "siteAccessProblem", source = "siteAccessProblem") + @Mapping(target = "needsInspection", source = "needsInspection") + @Mapping(target = "usagePointHrefs", source = "usagePointHrefs") + @Mapping(target = "outageBlock", source = "outageBlock") + @Mapping(target = "description", ignore = true) + @Mapping(target = "published", ignore = true) + @Mapping(target = "selfLink", ignore = true) + @Mapping(target = "upLink", ignore = true) + @Mapping(target = "created", ignore = true) + @Mapping(target = "updated", ignore = true) + ServiceLocationEntity toEntity(ServiceLocationDto dto); + + // Helper methods for embedded type conversions + + /** + * Maps Organisation.StreetAddress entity to CustomerDto.StreetAddressDto. + */ + default CustomerDto.StreetAddressDto map(Organisation.StreetAddress address) { + if (address == null) return null; + return new CustomerDto.StreetAddressDto( + address.getStreetDetail(), + address.getTownDetail(), + address.getStateOrProvince(), + address.getPostalCode(), + address.getCountry() + ); + } + + /** + * Maps CustomerDto.StreetAddressDto to Organisation.StreetAddress entity. + */ + default Organisation.StreetAddress map(CustomerDto.StreetAddressDto dto) { + if (dto == null) return null; + Organisation.StreetAddress address = new Organisation.StreetAddress(); + address.setStreetDetail(dto.getStreetDetail()); + address.setTownDetail(dto.getTownDetail()); + address.setStateOrProvince(dto.getStateOrProvince()); + address.setPostalCode(dto.getPostalCode()); + address.setCountry(dto.getCountry()); + return address; + } + + /** + * Maps Organisation.TelephoneNumber entity to CustomerDto.TelephoneNumberDto. + */ + default CustomerDto.TelephoneNumberDto mapTelephone(Organisation.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 Organisation.TelephoneNumber entity. + */ + default Organisation.TelephoneNumber mapTelephone(CustomerDto.TelephoneNumberDto dto) { + if (dto == null) return null; + return new Organisation.TelephoneNumber( + dto.getCountryCode(), + dto.getAreaCode(), + dto.getCityCode(), + dto.getLocalNumber(), + dto.getExt(), + dto.getDialOut(), + dto.getInternationalPrefix(), + dto.getItuPhone() + ); + } + + /** + * Maps Organisation.ElectronicAddress entity to CustomerDto.ElectronicAddressDto. + */ + default CustomerDto.ElectronicAddressDto mapElectronic(Organisation.ElectronicAddress address) { + if (address == null) return null; + return new CustomerDto.ElectronicAddressDto( + address.getLan(), + address.getMac(), + address.getEmail1(), + address.getEmail2(), + address.getWeb(), + address.getRadio(), + address.getUserID(), + address.getPassword() + ); + } + + /** + * Maps CustomerDto.ElectronicAddressDto to Organisation.ElectronicAddress entity. + */ + default Organisation.ElectronicAddress mapElectronic(CustomerDto.ElectronicAddressDto dto) { + if (dto == null) return null; + Organisation.ElectronicAddress address = new Organisation.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; + } + + /** + * Maps Status entity to StatusDto. + */ + default StatusDto mapStatus(Status status) { + if (status == null) return null; + return new StatusDto( + status.getValue(), + status.getDateTime(), + status.getRemark(), + status.getReason() + ); + } + + /** + * Maps StatusDto to Status entity. + */ + default Status mapStatus(StatusDto dto) { + if (dto == null) return null; + return new Status( + dto.getValue(), + dto.getDateTime(), + dto.getRemark(), + dto.getReason() + ); + } + + /** + * Maps list of PositionPoint entities to DTOs. + * TODO: Implement when PositionPointEntity is created. + */ + default List mapPositionPoints(List entities) { + // Placeholder - return null until PositionPointEntity is implemented + return null; + } + + /** + * Maps list of PositionPointDto to entities. + * TODO: Implement when PositionPointEntity is created. + */ + default List mapPositionPointsToEntities(List dtos) { + // Placeholder - return null until PositionPointEntity is implemented + return null; + } +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java index e163fb3d..b2b91af3 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepository.java @@ -32,54 +32,41 @@ * Spring Data JPA repository for ServiceLocationEntity entities. *

* Manages real estate location data including addresses, access methods, and service delivery points. + *

+ * Only contains queries on indexed fields to ensure efficient database access. + * Removed non-indexed queries in Phase 23 cleanup (findLocationsThatNeedInspection, + * findLocationsWithAccessProblems, findByMainAddressStreetContaining, findByDirectionContaining, + * findByPhone1AreaCode). */ @Repository public interface ServiceLocationRepository extends JpaRepository { /** * Find service locations by outage block. + * Outage block is indexed for efficient lookups. + * + * @param outageBlock the outage block identifier + * @return list of service locations in the outage block */ @Query("SELECT sl FROM ServiceLocationEntity sl WHERE sl.outageBlock = :outageBlock") List findByOutageBlock(@Param("outageBlock") String outageBlock); - /** - * Find service locations that need inspection. - */ - @Query("SELECT sl FROM ServiceLocationEntity sl WHERE sl.needsInspection = true") - List findLocationsThatNeedInspection(); - - /** - * Find service locations with access problems. - */ - @Query("SELECT sl FROM ServiceLocationEntity sl WHERE sl.siteAccessProblem IS NOT NULL AND sl.siteAccessProblem != ''") - List findLocationsWithAccessProblems(); - - /** - * Find service locations by main street address. - */ - @Query("SELECT sl FROM ServiceLocationEntity sl WHERE UPPER(sl.mainAddress.streetDetail) LIKE UPPER(CONCAT('%', :street, '%'))") - List findByMainAddressStreetContaining(@Param("street") String street); - - /** - * Find service locations by direction. - */ - @Query("SELECT sl FROM ServiceLocationEntity sl WHERE UPPER(sl.direction) LIKE UPPER(CONCAT('%', :direction, '%'))") - List findByDirectionContaining(@Param("direction") String direction); - /** * Find service locations by location type. + * Type is indexed for efficient classification lookups. + * + * @param type the location type classification + * @return list of service locations of the given type */ @Query("SELECT sl FROM ServiceLocationEntity sl WHERE sl.type = :type") List findByType(@Param("type") String type); - /** - * Find service locations by phone area code. - */ - @Query("SELECT sl FROM ServiceLocationEntity sl JOIN sl.phoneNumbers pn WHERE pn.areaCode = :areaCode") - List findByPhone1AreaCode(@Param("areaCode") String areaCode); - /** * Find service locations by geo info reference. + * Geo info reference is indexed for efficient geographic lookups. + * + * @param geoInfoReference the geographical information reference + * @return list of service locations with the given geo info reference */ @Query("SELECT sl FROM ServiceLocationEntity sl WHERE sl.geoInfoReference = :geoInfoReference") List findByGeoInfoReference(@Param("geoInfoReference") String geoInfoReference); diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ServiceLocationService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ServiceLocationService.java index 1d7ed25c..f06f0411 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ServiceLocationService.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ServiceLocationService.java @@ -53,36 +53,11 @@ public interface ServiceLocationService { */ List findByOutageBlock(String outageBlock); - /** - * Find service locations that need inspection. - */ - List findLocationsThatNeedInspection(); - - /** - * Find service locations with access problems. - */ - List findLocationsWithAccessProblems(); - - /** - * Find service locations by main address street. - */ - List findByMainAddressStreetContaining(String street); - - /** - * Find service locations by direction. - */ - List findByDirectionContaining(String direction); - /** * Find service locations by type. */ List findByType(String type); - /** - * Find service locations by phone area code. - */ - List findByPhone1AreaCode(String areaCode); - /** * Find service locations by geo info reference. */ @@ -112,14 +87,4 @@ public interface ServiceLocationService { * Count total service locations. */ long countServiceLocationEntitys(); - - /** - * Count locations needing inspection. - */ - long countLocationsNeedingInspection(); - - /** - * Count locations with access problems. - */ - long countLocationsWithAccessProblems(); } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ServiceLocationServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ServiceLocationServiceImpl.java index dced52d2..1a78439c 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ServiceLocationServiceImpl.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ServiceLocationServiceImpl.java @@ -66,42 +66,12 @@ public List findByOutageBlock(String outageBlock) { return serviceLocationRepository.findByOutageBlock(outageBlock); } - @Override - @Transactional(readOnly = true) - public List findLocationsThatNeedInspection() { - return serviceLocationRepository.findLocationsThatNeedInspection(); - } - - @Override - @Transactional(readOnly = true) - public List findLocationsWithAccessProblems() { - return serviceLocationRepository.findLocationsWithAccessProblems(); - } - - @Override - @Transactional(readOnly = true) - public List findByMainAddressStreetContaining(String street) { - return serviceLocationRepository.findByMainAddressStreetContaining(street); - } - - @Override - @Transactional(readOnly = true) - public List findByDirectionContaining(String direction) { - return serviceLocationRepository.findByDirectionContaining(direction); - } - @Override @Transactional(readOnly = true) public List findByType(String type) { return serviceLocationRepository.findByType(type); } - @Override - @Transactional(readOnly = true) - public List findByPhone1AreaCode(String areaCode) { - return serviceLocationRepository.findByPhone1AreaCode(areaCode); - } - @Override @Transactional(readOnly = true) public List findByGeoInfoReference(String geoInfoReference) { @@ -149,16 +119,4 @@ public ServiceLocationEntity updateAccessProblem(UUID id, String accessProblem) public long countServiceLocationEntitys() { return serviceLocationRepository.count(); } - - @Override - @Transactional(readOnly = true) - public long countLocationsNeedingInspection() { - return serviceLocationRepository.findLocationsThatNeedInspection().size(); - } - - @Override - @Transactional(readOnly = true) - public long countLocationsWithAccessProblems() { - return serviceLocationRepository.findLocationsWithAccessProblems().size(); - } } \ 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 15358684..f4d6feb5 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 @@ -264,6 +264,7 @@ private Marshaller createMarshaller(Class dtoClass, Set requiredNames org.greenbuttonalliance.espi.common.dto.customer.ServiceLocationDto.class, org.greenbuttonalliance.espi.common.dto.customer.StatementDto.class, org.greenbuttonalliance.espi.common.dto.customer.StatementRefDto.class, + org.greenbuttonalliance.espi.common.dto.customer.StatusDto.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 ca61474a..9b0c2490 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 @@ -645,11 +645,32 @@ CREATE TABLE service_locations electronic_user_id VARCHAR(255), electronic_password VARCHAR(255), - -- Status embedded object columns + -- Status embedded object columns (customer.xsd Status type - 4 fields) status_value VARCHAR(256), status_date_time TIMESTAMP, + status_remark VARCHAR(256), 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), + + -- 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), + -- Service location specific fields access_method VARCHAR(256), site_access_problem VARCHAR(256), @@ -666,6 +687,7 @@ CREATE INDEX idx_service_location_updated ON service_locations (updated); -- Related Links Table for Service Locations +-- Stores Atom elements per NAESB ESPI 4.0 REQ.21.4.3.1.1.6 CREATE TABLE service_location_related_links ( service_location_id CHAR(36) NOT NULL, @@ -676,6 +698,19 @@ CREATE TABLE service_location_related_links CREATE INDEX idx_service_location_related_links ON service_location_related_links (service_location_id); +-- UsagePoint Hrefs Table for Service Locations +-- Cross-stream references from customer.xsd to usage.xsd +-- Stores UsagePoint atom:link[@rel='self']/@href URLs per customer.xsd lines 1106-1111 +CREATE TABLE service_location_usage_point_hrefs +( + service_location_id CHAR(36) NOT NULL, + usage_point_href VARCHAR(512), + FOREIGN KEY (service_location_id) REFERENCES service_locations (id) ON DELETE CASCADE +); + +CREATE INDEX idx_service_location_usage_point_hrefs ON service_location_usage_point_hrefs (service_location_id); + + -- Service Supplier Table CREATE TABLE service_suppliers ( 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 cf56871a..52cc4582 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 @@ -55,10 +55,11 @@ void setUp() { @DisplayName("Should marshal Customer with all fields populated") void shouldMarshalCustomerWithAllFields() { // Arrange - Create comprehensive CustomerDto with all fields - CustomerDto.StatusDto status = new CustomerDto.StatusDto( + StatusDto status = new StatusDto( "active", OffsetDateTime.now(), - "New account created" + "New account created", + null ); CustomerDto.PriorityDto priority = new CustomerDto.PriorityDto( @@ -67,17 +68,25 @@ void shouldMarshalCustomerWithAllFields() { "high-priority" // type ); - CustomerDto.PhoneNumberDto phone1 = new CustomerDto.PhoneNumberDto( + CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + "1", // countryCode "415", // areaCode "555", // cityCode "1234", // localNumber - "100" // extension + "100", // ext + null, // dialOut + null, // internationalPrefix + null // ituPhone ); - CustomerDto.PhoneNumberDto phone2 = new CustomerDto.PhoneNumberDto( + CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + "1", "415", "555", "5678", + null, + null, + null, null ); 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 f3e46ea1..511c1999 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 @@ -273,12 +273,12 @@ private CustomerDto createFullCustomerDto() { "PO Box 456", "Springfield", "IL", "62702", "USA" ); - CustomerDto.PhoneNumberDto phone1 = new CustomerDto.PhoneNumberDto( - "217", null, "555-1234", null + CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + "1", "217", null, "555-1234", null, null, null, null ); - CustomerDto.PhoneNumberDto phone2 = new CustomerDto.PhoneNumberDto( - "217", null, "555-5678", "101" + CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + "1", "217", null, "555-5678", "101", null, null, null ); CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( @@ -289,10 +289,11 @@ private CustomerDto createFullCustomerDto() { streetAddress, postalAddress, phone1, phone2, electronicAddress, "ACME Energy Services" ); - CustomerDto.StatusDto status = new CustomerDto.StatusDto( + StatusDto status = new StatusDto( "ACTIVE", OffsetDateTime.of(2025, 1, 15, 10, 30, 0, 0, ZoneOffset.UTC), - "Account in good standing" + "Account in good standing", + null ); CustomerDto.PriorityDto priority = new CustomerDto.PriorityDto(5, 1, "STANDARD"); 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 new file mode 100644 index 00000000..b3eb619e --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java @@ -0,0 +1,374 @@ +/* + * + * 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.dto.atom.AtomFeedDto; +import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; +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 ServiceLocationDto. + * Verifies Jakarta JAXB Marshaller processes JAXB annotations correctly for ESPI 4.0 customer.xsd compliance. + * Tests Phase 23 compliance: ServiceLocation extends WorkLocation extends Location. + */ +@DisplayName("ServiceLocationDto XML Marshalling Tests - Phase 23") +class ServiceLocationDtoTest { + + 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 ServiceLocation with complete realistic data") + void shouldExportServiceLocationWithRealisticData() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceLocationDto serviceLocation = createFullServiceLocationDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:650e8400-e29b-51d4-a716-446655440000", + "ACME Energy Service Location", + now, now, null, serviceLocation + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "ServiceLocation 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("========== ServiceLocation XML Output =========="); + System.out.println(xml); + System.out.println("==============================================="); + + // Assert - Basic structure and content + assertThat(xml) + .startsWith("") + .contains(""); + int mainAddressPos = xml.indexOf(""); + int secondaryAddressPos = xml.indexOf(""); + int phone1Pos = xml.indexOf(""); + int phone2Pos = xml.indexOf(""); + int electronicAddressPos = xml.indexOf(""); + int geoInfoReferencePos = xml.indexOf(""); + int directionPos = xml.indexOf(""); + int statusPos = xml.indexOf(""); + int accessMethodPos = xml.indexOf(""); + int siteAccessProblemPos = xml.indexOf(""); + int needsInspectionPos = xml.indexOf(""); + int usagePointsPos = xml.indexOf(""); + int outageBlockPos = xml.indexOf(""); + + // Verify Location field order + assertThat(typePos).isGreaterThan(0).isLessThan(mainAddressPos); + assertThat(mainAddressPos).isLessThan(secondaryAddressPos); + assertThat(secondaryAddressPos).isLessThan(phone1Pos); + assertThat(phone1Pos).isLessThan(phone2Pos); + assertThat(phone2Pos).isLessThan(electronicAddressPos); + assertThat(electronicAddressPos).isLessThan(geoInfoReferencePos); + assertThat(geoInfoReferencePos).isLessThan(directionPos); + assertThat(directionPos).isLessThan(statusPos); + + // Verify ServiceLocation field order (after Location fields) + assertThat(statusPos).isLessThan(accessMethodPos); + assertThat(accessMethodPos).isLessThan(siteAccessProblemPos); + assertThat(siteAccessProblemPos).isLessThan(needsInspectionPos); + assertThat(needsInspectionPos).isLessThan(usagePointsPos); + assertThat(usagePointsPos).isLessThan(outageBlockPos); + } + + @Test + @DisplayName("Should verify TelephoneNumber 8-field compliance") + void shouldVerifyTelephoneNumber8FieldCompliance() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceLocationDto serviceLocation = createFullServiceLocationDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:650e8400-e29b-51d4-a716-446655440002", + "Test ServiceLocation", + now, now, null, serviceLocation + ); + + 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 all 8 TelephoneNumber fields present per customer.xsd:1428-1478 + assertThat(xml) + .contains("1") + .contains("312") + .containsAnyOf("", "") + .contains("555-1000") + .containsAnyOf("", "") + .containsAnyOf("", "") + .containsAnyOf("", "") + .containsAnyOf("", ""); + } + + @Test + @DisplayName("Should verify Status 4-field compliance") + void shouldVerifyStatus4FieldCompliance() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceLocationDto serviceLocation = createFullServiceLocationDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:650e8400-e29b-51d4-a716-446655440003", + "Test ServiceLocation", + now, now, null, serviceLocation + ); + + 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 all 4 Status fields present (value, dateTime, remark, reason) + assertThat(xml) + .contains("") + .contains("ACTIVE") + .containsAnyOf("", "") + .contains("Location is operational") + .contains("Routine inspection passed"); + } + + @Test + @DisplayName("Should export ServiceLocation with minimal data") + void shouldExportServiceLocationWithMinimalData() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + ServiceLocationDto serviceLocation = createMinimalServiceLocationDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:650e8400-e29b-51d4-a716-446655440004", + "Minimal ServiceLocation", + now, now, null, serviceLocation + ); + + 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 - ServiceLocation element present (may be self-closing) + assertThat(xml).contains("") + .contains("https://api.example.com/espi/1_1/resource/UsagePoint/12345") + .contains("https://api.example.com/espi/1_1/resource/UsagePoint/67890"); + } + + /** + * Creates a full ServiceLocationDto with all Location and ServiceLocation fields populated. + * Per ESPI 4.0 customer.xsd: ServiceLocation → WorkLocation → Location → IdentifiedObject. + */ + private ServiceLocationDto createFullServiceLocationDto() { + // Location fields + CustomerDto.StreetAddressDto mainAddress = new CustomerDto.StreetAddressDto( + "456 Industrial Blvd", "Chicago", "IL", "60601", "USA" + ); + + CustomerDto.StreetAddressDto secondaryAddress = new CustomerDto.StreetAddressDto( + "PO Box 789", "Chicago", "IL", "60602", "USA" + ); + + CustomerDto.TelephoneNumberDto phone1 = new CustomerDto.TelephoneNumberDto( + "1", "312", "773", "555-1000", "100", "9", "011", "+1-312-555-1000" + ); + + CustomerDto.TelephoneNumberDto phone2 = new CustomerDto.TelephoneNumberDto( + "1", "312", "773", "555-2000", "200", "9", "011", "+1-312-555-2000" + ); + + CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + "192.168.1.100", "00:11:22:33:44:55", "meter@example.com", "support@example.com", + "https://meter.example.com", "VHF-123", "meter_user", null + ); + + StatusDto status = new StatusDto( + "ACTIVE", + OffsetDateTime.of(2025, 1, 20, 14, 30, 0, 0, ZoneOffset.UTC), + "Location is operational", + "Routine inspection passed" + ); + + ServiceLocationDto.PositionPointDto positionPoint1 = new ServiceLocationDto.PositionPointDto( + "41.8781", "-87.6298", null + ); + + ServiceLocationDto.PositionPointDto positionPoint2 = new ServiceLocationDto.PositionPointDto( + "41.8782", "-87.6299", "100" + ); + + List positionPoints = List.of(positionPoint1, positionPoint2); + + // ServiceLocation fields + List usagePointHrefs = List.of( + "https://api.example.com/espi/1_1/resource/UsagePoint/12345", + "https://api.example.com/espi/1_1/resource/UsagePoint/67890" + ); + + return new ServiceLocationDto( + null, // id + "650e8400-e29b-51d4-a716-446655440000", // uuid + "COMMERCIAL", // type + mainAddress, + secondaryAddress, + phone1, + phone2, + electronicAddress, + "GEO-REF-12345", // geoInfoReference + "North side of building, meter room on 2nd floor", // direction + status, + positionPoints, + "Call office for key", // accessMethod + "Guard dogs on premises", // siteAccessProblem + true, // needsInspection + usagePointHrefs, + "OUTAGE-BLOCK-001" // outageBlock + ); + } + + /** + * Creates a minimal ServiceLocationDto with only required fields. + */ + private ServiceLocationDto createMinimalServiceLocationDto() { + return new ServiceLocationDto( + null, // id + "650e8400-e29b-51d4-a716-446655440099", // uuid + null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null + ); + } +} 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 ab66b976..30d698d7 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 @@ -270,126 +270,6 @@ void shouldFindServiceLocationsByOutageBlock() { assertThat(results).allMatch(loc -> loc.getOutageBlock().equals(outageBlock)); } - @Test - @DisplayName("Should find locations that need inspection") - void shouldFindLocationsThatNeedInspection() { - // Arrange - ServiceLocationEntity needsInspection1 = createValidServiceLocation(); - needsInspection1.setNeedsInspection(true); - needsInspection1.setDescription("Needs Inspection 1"); - - ServiceLocationEntity needsInspection2 = createValidServiceLocation(); - needsInspection2.setNeedsInspection(true); - needsInspection2.setDescription("Needs Inspection 2"); - - ServiceLocationEntity noInspection = createValidServiceLocation(); - noInspection.setNeedsInspection(false); - noInspection.setDescription("No Inspection Needed"); - - serviceLocationRepository.saveAll(List.of(needsInspection1, needsInspection2, noInspection)); - flushAndClear(); - - // Act - List results = serviceLocationRepository.findLocationsThatNeedInspection(); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(ServiceLocationEntity::getDescription) - .contains("Needs Inspection 1", "Needs Inspection 2"); - assertThat(results).allMatch(loc -> loc.getNeedsInspection() == true); - } - - @Test - @DisplayName("Should find locations with access problems") - void shouldFindLocationsWithAccessProblems() { - // Arrange - ServiceLocationEntity withProblem1 = createValidServiceLocation(); - withProblem1.setSiteAccessProblem("Aggressive dog"); - withProblem1.setDescription("Location with Problem 1"); - - ServiceLocationEntity withProblem2 = createValidServiceLocation(); - withProblem2.setSiteAccessProblem("Locked gate"); - withProblem2.setDescription("Location with Problem 2"); - - ServiceLocationEntity noProblem = createValidServiceLocation(); - noProblem.setSiteAccessProblem(null); - noProblem.setDescription("Location without Problem"); - - serviceLocationRepository.saveAll(List.of(withProblem1, withProblem2, noProblem)); - flushAndClear(); - - // Act - List results = serviceLocationRepository.findLocationsWithAccessProblems(); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(ServiceLocationEntity::getDescription) - .contains("Location with Problem 1", "Location with Problem 2"); - assertThat(results).allMatch(loc -> loc.getSiteAccessProblem() != null && !loc.getSiteAccessProblem().isEmpty()); - } - - @Test - @DisplayName("Should find service locations by main address street containing") - void shouldFindServiceLocationsByMainAddressStreetContaining() { - // Arrange - ServiceLocationEntity location1 = createValidServiceLocation(); - Organisation.StreetAddress address1 = new Organisation.StreetAddress(); - address1.setStreetDetail("123 Main Street"); - location1.setMainAddress(address1); - location1.setDescription("Main Street Location 1"); - - ServiceLocationEntity location2 = createValidServiceLocation(); - Organisation.StreetAddress address2 = new Organisation.StreetAddress(); - address2.setStreetDetail("456 Main Avenue"); - location2.setMainAddress(address2); - location2.setDescription("Main Street Location 2"); - - ServiceLocationEntity location3 = createValidServiceLocation(); - Organisation.StreetAddress address3 = new Organisation.StreetAddress(); - address3.setStreetDetail("789 Oak Street"); - location3.setMainAddress(address3); - location3.setDescription("Oak Street Location"); - - serviceLocationRepository.saveAll(List.of(location1, location2, location3)); - flushAndClear(); - - // Act - List results = serviceLocationRepository.findByMainAddressStreetContaining("Main"); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(ServiceLocationEntity::getDescription) - .contains("Main Street Location 1", "Main Street Location 2"); - } - - @Test - @DisplayName("Should find service locations by direction containing") - void shouldFindServiceLocationsByDirectionContaining() { - // Arrange - ServiceLocationEntity location1 = createValidServiceLocation(); - location1.setDirection("North side of building"); - location1.setDescription("North Location 1"); - - ServiceLocationEntity location2 = createValidServiceLocation(); - location2.setDirection("North entrance"); - location2.setDescription("North Location 2"); - - ServiceLocationEntity location3 = createValidServiceLocation(); - location3.setDirection("South entrance"); - location3.setDescription("South Location"); - - serviceLocationRepository.saveAll(List.of(location1, location2, location3)); - flushAndClear(); - - // Act - List results = serviceLocationRepository.findByDirectionContaining("North"); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(ServiceLocationEntity::getDescription) - .contains("North Location 1", "North Location 2"); - } - @Test @DisplayName("Should find service locations by type") void shouldFindServiceLocationsByType() { @@ -482,9 +362,6 @@ void shouldHandleEmptyResultsGracefully() { // Act & Assert assertThat(serviceLocationRepository.findByOutageBlock("nonexistent-block")).isEmpty(); assertThat(serviceLocationRepository.findByType("NonexistentType")).isEmpty(); - assertThat(serviceLocationRepository.findByDirectionContaining("nonexistent")).isEmpty(); - assertThat(serviceLocationRepository.findByMainAddressStreetContaining("nonexistent")).isEmpty(); - assertThat(serviceLocationRepository.findByPhone1AreaCode("999")).isEmpty(); assertThat(serviceLocationRepository.findByGeoInfoReference("nonexistent-ref")).isEmpty(); } } @@ -525,7 +402,7 @@ void shouldHandleServiceLocationWithStatus() { // Arrange ServiceLocationEntity serviceLocation = createValidServiceLocation(); - CustomerEntity.Status status = new CustomerEntity.Status(); + Status status = new Status(); status.setValue("Active"); status.setReason("Normal operation"); serviceLocation.setStatus(status);