Skip to content

Commit 4441d92

Browse files
dfcoffinclaude
andauthored
feat: ESPI 4.0 Schema Compliance - Phase 19: Statement Complete Implementation (#102)
* feat: ESPI 4.0 Schema Compliance - Phase 19: Statement Complete Implementation Complete implementation of Statement and StatementRef entities per customer.xsd specification (lines 285-307, 373-393) following Phase 17/18 patterns. Schema Compliance Changes: - StatementRefEntity: Changed from @entity to @embeddable (extends Object, not IdentifiedObject) - Removed UUID primary key, now stored as @ElementCollection in StatementEntity - statement_refs table: Removed id column, now collection table with statement_id FK only - StatementDto: Removed ALL IdentifiedObject fields, keeps ONLY issueDateTime + statementRef per XSD - StatementRefDto: Fixed to match XSD (fileName, mediaType, statementURL only) Entity Enhancements: - Added @manytoone relationships for Controller API navigation: * customer_id → CustomerEntity * customer_account_id → CustomerAccountEntity * customer_agreement_id → CustomerAgreementEntity * usage_summary_id → UsageSummaryEntity - Updated toString() to follow database field sequence per CLAUDE.md guidelines - @AssociationOverride for statement_related_links (bidirectional Atom protocol links) Database Migration (V3): - statements table: Added customer_account_id, customer_agreement_id, usage_summary_id FKs - Removed non-schema fields: statement_date, billing_period_start, billing_period_duration - statement_refs table: Restructured as @ElementCollection table (no id column) - Added indexes for all relationship foreign keys Mapper Implementation: - StatementMapper: Maps ONLY customer.xsd fields (issueDateTime, statementRef) - StatementRefMapper: Direct 1:1 field mapping (3 fields) - Uses qualifiedByName for DateTimeMapper epoch conversions - Follows DRY principle - Atom metadata handled by AtomEntryDto/LinkDto Repository & Service Simplification: - Removed 7 non-ID query methods from StatementRepository - Added 4 ID-based relationship queries (Customer, CustomerAccount, CustomerAgreement, UsageSummary) - Removed 10 methods from StatementService (date/description/count queries) - Kept only CRUD + ID-based operations per ESPI compliance patterns Resolves #28 Phase 19 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * test: Add comprehensive test suite for Phase 19 Statement implementation Test Coverage: - StatementRepositoryTest: 7 tests for CRUD + ID-based relationship queries - StatementPostgreSQLIntegrationTest: 4 tests with real PostgreSQL TestContainer - StatementMySQLIntegrationTest: 4 tests with real MySQL TestContainer Key Test Scenarios: - @ElementCollection StatementRef persistence (no id column) - All 4 relationship queries (Customer, CustomerAccount, CustomerAgreement, UsageSummary) - Cascading delete of @ElementCollection - CRUD operations with real databases Results: 781 total tests passing (added 15 new tests) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: Truncate timestamps to microseconds for cross-platform compatibility Issue: CI/CD failing due to timestamp precision mismatch between platforms - Windows: microseconds (6 decimals) - Linux/Mac: nanoseconds (9 decimals) Solution: Truncate all test timestamps to MICROS using .truncatedTo(ChronoUnit.MICROS) Changes: - StatementRepositoryTest: 3 occurrences fixed - StatementMySQLIntegrationTest: 3 occurrences fixed - StatementPostgreSQLIntegrationTest: 3 occurrences fixed Results: All 781 tests passing locally Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 18a8e8b commit 4441d92

13 files changed

Lines changed: 926 additions & 841 deletions

File tree

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,44 @@ public class StatementEntity extends IdentifiedObject {
5656

5757
/**
5858
* [extension] Contains document reference metadata needed to access a document representation of a billing statement.
59+
* StatementRef extends Object (not IdentifiedObject), so stored as @ElementCollection.
5960
*/
60-
@OneToMany(mappedBy = "statement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
61+
@ElementCollection(fetch = FetchType.LAZY)
62+
@CollectionTable(name = "statement_refs", joinColumns = @JoinColumn(name = "statement_id"))
6163
private List<StatementRefEntity> statementRefs;
6264

6365
/**
64-
* Customer that owns this statement.
66+
* Customer associated with this statement.
6567
* Many statements can belong to one customer.
6668
*/
6769
@ManyToOne(fetch = FetchType.LAZY)
6870
@JoinColumn(name = "customer_id")
6971
private CustomerEntity customer;
7072

73+
/**
74+
* Customer account associated with this statement.
75+
* Many statements can belong to one customer account.
76+
*/
77+
@ManyToOne(fetch = FetchType.LAZY)
78+
@JoinColumn(name = "customer_account_id")
79+
private CustomerAccountEntity customerAccount;
80+
81+
/**
82+
* Customer agreement associated with this statement.
83+
* Many statements can belong to one customer agreement.
84+
*/
85+
@ManyToOne(fetch = FetchType.LAZY)
86+
@JoinColumn(name = "customer_agreement_id")
87+
private CustomerAgreementEntity customerAgreement;
88+
89+
/**
90+
* Usage summary associated with this statement.
91+
* Many statements can belong to one usage summary.
92+
*/
93+
@ManyToOne(fetch = FetchType.LAZY)
94+
@JoinColumn(name = "usage_summary_id")
95+
private org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity usageSummary;
96+
7197
@Override
7298
public final boolean equals(Object o) {
7399
if (this == o) return true;
@@ -88,10 +114,13 @@ public final int hashCode() {
88114
public String toString() {
89115
return getClass().getSimpleName() + "(" +
90116
"id = " + getId() + ", " +
91-
"issueDateTime = " + getIssueDateTime() + ", " +
92117
"description = " + getDescription() + ", " +
93118
"created = " + getCreated() + ", " +
94119
"updated = " + getUpdated() + ", " +
95-
"published = " + getPublished() + ")";
120+
"published = " + getPublished() + ", " +
121+
"upLink = " + getUpLink() + ", " +
122+
"selfLink = " + getSelfLink() + ", " +
123+
"issueDateTime = " + getIssueDateTime() + ", " +
124+
"relatedLinks = " + getRelatedLinks() + ")";
96125
}
97126
}

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java

Lines changed: 14 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,28 @@
1919

2020
package org.greenbuttonalliance.espi.common.domain.customer.entity;
2121

22-
import jakarta.persistence.*;
23-
import lombok.Getter;
22+
import jakarta.persistence.Column;
23+
import jakarta.persistence.Embeddable;
24+
import lombok.Data;
2425
import lombok.NoArgsConstructor;
25-
import lombok.Setter;
26-
import org.hibernate.annotations.JdbcTypeCode;
27-
import org.hibernate.proxy.HibernateProxy;
28-
import org.hibernate.type.SqlTypes;
26+
import lombok.ToString;
2927

30-
import java.util.Objects;
31-
import java.util.UUID;
28+
import java.io.Serializable;
3229

3330
/**
34-
* Pure JPA/Hibernate entity for StatementRef without JAXB concerns.
31+
* Embeddable class for StatementRef without JAXB concerns.
3532
*
3633
* [extension] A sequence of references to a document associated with a Statement.
3734
*
38-
* Note: StatementRef does NOT extend IdentifiedObject per ESPI 4.0 specification.
39-
* It is not a top-level resource with selfLink/upLink/relatedLinks.
35+
* Note: StatementRef extends Object (not IdentifiedObject) per customer.xsd lines 285-307.
36+
* It is not a top-level resource and has no selfLink/upLink/relatedLinks.
37+
* Stored as @ElementCollection in StatementEntity.
4038
*/
41-
@Entity
42-
@Table(name = "statement_refs")
43-
@Getter
44-
@Setter
39+
@Embeddable
40+
@Data
4541
@NoArgsConstructor
46-
public class StatementRefEntity {
47-
48-
/**
49-
* Primary key identifier.
50-
*/
51-
@Id
52-
@GeneratedValue(strategy = GenerationType.UUID)
53-
@JdbcTypeCode(SqlTypes.CHAR)
54-
@Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false)
55-
private UUID id;
42+
@ToString
43+
public class StatementRefEntity implements Serializable {
5644

5745
/**
5846
* [extension] Name of document or file including filename extension if present.
@@ -67,44 +55,9 @@ public class StatementRefEntity {
6755
private String mediaType;
6856

6957
/**
70-
* [extension] URL used to access a representation of a statement, for example a bill image.
58+
* [extension] URL used to access a representation of a statement, for example a bill image.
7159
* Use CDATA or URL encoding to escape characters not allowed in XML.
7260
*/
7361
@Column(name = "statement_url", length = 2048)
7462
private String statementURL;
75-
76-
/**
77-
* Statement this reference belongs to
78-
*/
79-
@ManyToOne(fetch = FetchType.LAZY)
80-
@JoinColumn(name = "statement_id")
81-
private StatementEntity statement;
82-
83-
@Override
84-
public final boolean equals(Object o) {
85-
if (this == o) return true;
86-
if (o == null) return false;
87-
Class<?> oEffectiveClass = o instanceof HibernateProxy hibernateProxy ?
88-
hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass();
89-
Class<?> thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ?
90-
hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass();
91-
if (thisEffectiveClass != oEffectiveClass) return false;
92-
StatementRefEntity that = (StatementRefEntity) o;
93-
return getId() != null && Objects.equals(getId(), that.getId());
94-
}
95-
96-
@Override
97-
public final int hashCode() {
98-
return this instanceof HibernateProxy hibernateProxy ?
99-
hibernateProxy.getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
100-
}
101-
102-
@Override
103-
public String toString() {
104-
return getClass().getSimpleName() + "(" +
105-
"id = " + getId() + ", " +
106-
"fileName = " + getFileName() + ", " +
107-
"mediaType = " + getMediaType() + ", " +
108-
"statementURL = " + getStatementURL() + ")";
109-
}
11063
}

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java

Lines changed: 21 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -19,109 +19,50 @@
1919

2020
package org.greenbuttonalliance.espi.common.dto.customer;
2121

22-
import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;
23-
2422
import jakarta.xml.bind.annotation.*;
2523
import lombok.AllArgsConstructor;
2624
import lombok.Getter;
2725
import lombok.NoArgsConstructor;
2826
import lombok.Setter;
2927

30-
import java.time.OffsetDateTime;
3128
import java.util.List;
3229

3330
/**
34-
* Statement DTO class for JAXB XML marshalling/unmarshalling.
31+
* Statement DTO for JAXB XML marshalling/unmarshalling.
32+
*
33+
* [extension] Billing statement for provided services.
34+
*
35+
* Corresponds to customer.xsd Statement definition (lines 373-393).
36+
* Statement extends IdentifiedObject, but DTO excludes IdentifiedObject fields
37+
* (id, description, published, updated, links) as they're handled by Atom Entry wrapper.
3538
*
36-
* Represents a billing statement or document for a customer agreement.
37-
* Supports Atom protocol XML wrapping.
39+
* ESPI 4.0 XSD Sequence (customer.xsd lines 379-392):
40+
* 1. issueDateTime (TimeType) - optional
41+
* 2. statementRef (StatementRef collection) - optional, unbounded
3842
*/
3943
@XmlRootElement(name = "Statement", namespace = "http://naesb.org/espi/customer")
4044
@XmlAccessorType(XmlAccessType.FIELD)
4145
@XmlType(name = "Statement", namespace = "http://naesb.org/espi/customer", propOrder = {
42-
"published", "updated", "selfLink", "upLink", "relatedLinks",
43-
"description", "createdDateTime", "lastModifiedDateTime", "revisionNumber",
44-
"subject", "docStatus", "type", "customerAgreement", "statementRefs"
46+
"issueDateTime",
47+
"statementRef"
4548
})
4649
@Getter
4750
@Setter
4851
@NoArgsConstructor
4952
@AllArgsConstructor
5053
public class StatementDto {
5154

52-
@XmlTransient
53-
private Long id;
54-
55-
@XmlAttribute(name = "mRID")
56-
private String uuid;
57-
58-
@XmlElement(name = "published")
59-
private OffsetDateTime published;
60-
61-
@XmlElement(name = "updated")
62-
private OffsetDateTime updated;
63-
64-
@XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
65-
@XmlElementWrapper(name = "links", namespace = "http://www.w3.org/2005/Atom")
66-
private List<LinkDto> relatedLinks;
67-
68-
@XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
69-
private LinkDto selfLink;
70-
71-
@XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
72-
private LinkDto upLink;
73-
74-
@XmlElement(name = "description")
75-
private String description;
76-
77-
@XmlElement(name = "createdDateTime")
78-
private OffsetDateTime createdDateTime;
79-
80-
@XmlElement(name = "lastModifiedDateTime")
81-
private OffsetDateTime lastModifiedDateTime;
82-
83-
@XmlElement(name = "revisionNumber")
84-
private String revisionNumber;
85-
86-
@XmlElement(name = "subject")
87-
private String subject;
88-
89-
@XmlElement(name = "docStatus")
90-
private String docStatus;
91-
92-
@XmlElement(name = "type")
93-
private String type;
94-
95-
@XmlElement(name = "CustomerAgreement")
96-
private CustomerAgreementDto customerAgreement;
97-
98-
@XmlElement(name = "StatementRef")
99-
@XmlElementWrapper(name = "StatementRefs")
100-
private List<StatementRefDto> statementRefs;
101-
102-
/**
103-
* Minimal constructor for basic statement data.
104-
*/
105-
public StatementDto(String uuid, String subject) {
106-
this(null, uuid, null, null, null, null, null, null,
107-
null, null, null, subject, null, null, null, null);
108-
}
109-
11055
/**
111-
* Gets the self href for this statement.
112-
*
113-
* @return self href string
56+
* [extension] Date and time at which a billing statement was issued.
57+
* Stored as Unix epoch timestamp (seconds since 1970-01-01T00:00:00Z).
11458
*/
115-
public String getSelfHref() {
116-
return selfLink != null ? selfLink.getHref() : null;
117-
}
59+
@XmlElement(name = "issueDateTime", namespace = "http://naesb.org/espi/customer")
60+
private Long issueDateTime;
11861

11962
/**
120-
* Gets the up href for this statement.
121-
*
122-
* @return up href string
63+
* [extension] Contains document reference metadata needed to access a document
64+
* representation of a billing statement.
12365
*/
124-
public String getUpHref() {
125-
return upLink != null ? upLink.getHref() : null;
126-
}
127-
}
66+
@XmlElement(name = "statementRef", namespace = "http://naesb.org/espi/customer")
67+
private List<StatementRefDto> statementRef;
68+
}

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,51 +25,49 @@
2525
import lombok.NoArgsConstructor;
2626
import lombok.Setter;
2727

28-
import java.time.OffsetDateTime;
29-
3028
/**
31-
* StatementRef DTO class for JAXB XML marshalling/unmarshalling.
29+
* StatementRef DTO for JAXB XML marshalling/unmarshalling.
3230
*
33-
* Represents a reference to a statement document.
31+
* [extension] A sequence of references to a document associated with a Statement.
3432
*
35-
* Note: StatementRef does NOT extend IdentifiedObject per ESPI 4.0 specification.
36-
* It is not a top-level resource with selfLink/upLink/relatedLinks.
33+
* Corresponds to customer.xsd StatementRef definition (lines 285-307).
34+
* StatementRef extends Object (not IdentifiedObject), so it has no id/links/metadata.
3735
*
38-
* WARNING: DTO fields do not currently match entity fields.
39-
* Entity has: fileName, mediaType, statementURL
40-
* DTO has: referenceId, referenceType, referenceDate, referenceUrl
41-
* This mismatch needs to be resolved.
36+
* ESPI 4.0 XSD Sequence (customer.xsd lines 291-306):
37+
* 1. fileName (String256) - optional
38+
* 2. mediaType (String256) - optional
39+
* 3. statementURL (String2048) - optional
4240
*/
4341
@XmlRootElement(name = "StatementRef", namespace = "http://naesb.org/espi/customer")
4442
@XmlAccessorType(XmlAccessType.FIELD)
4543
@XmlType(name = "StatementRef", namespace = "http://naesb.org/espi/customer", propOrder = {
46-
"referenceId", "referenceType", "referenceDate", "referenceUrl", "statement"
44+
"fileName",
45+
"mediaType",
46+
"statementURL"
4747
})
4848
@Getter
4949
@Setter
5050
@NoArgsConstructor
5151
@AllArgsConstructor
5252
public class StatementRefDto {
5353

54-
@XmlElement(name = "referenceId")
55-
private String referenceId;
56-
57-
@XmlElement(name = "referenceType")
58-
private String referenceType;
59-
60-
@XmlElement(name = "referenceDate")
61-
private OffsetDateTime referenceDate;
62-
63-
@XmlElement(name = "referenceUrl")
64-
private String referenceUrl;
54+
/**
55+
* [extension] Name of document or file including filename extension if present.
56+
*/
57+
@XmlElement(name = "fileName", namespace = "http://naesb.org/espi/customer")
58+
private String fileName;
6559

66-
@XmlElement(name = "Statement")
67-
private StatementDto statement;
60+
/**
61+
* [extension] Document media type as published by IANA.
62+
* See https://www.iana.org/assignments/media-types for more information.
63+
*/
64+
@XmlElement(name = "mediaType", namespace = "http://naesb.org/espi/customer")
65+
private String mediaType;
6866

6967
/**
70-
* Minimal constructor for basic reference data.
68+
* [extension] URL used to access a representation of a statement, for example a bill image.
69+
* Use CDATA or URL encoding to escape characters not allowed in XML.
7170
*/
72-
public StatementRefDto(String referenceId, String referenceUrl) {
73-
this(referenceId, null, null, referenceUrl, null);
74-
}
75-
}
71+
@XmlElement(name = "statementURL", namespace = "http://naesb.org/espi/customer")
72+
private String statementURL;
73+
}

0 commit comments

Comments
 (0)