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);