diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java index afd1e57f..589e6df4 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java @@ -56,18 +56,44 @@ public class StatementEntity extends IdentifiedObject { /** * [extension] Contains document reference metadata needed to access a document representation of a billing statement. + * StatementRef extends Object (not IdentifiedObject), so stored as @ElementCollection. */ - @OneToMany(mappedBy = "statement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "statement_refs", joinColumns = @JoinColumn(name = "statement_id")) private List statementRefs; /** - * Customer that owns this statement. + * Customer associated with this statement. * Many statements can belong to one customer. */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") private CustomerEntity customer; + /** + * Customer account associated with this statement. + * Many statements can belong to one customer account. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customer_account_id") + private CustomerAccountEntity customerAccount; + + /** + * Customer agreement associated with this statement. + * Many statements can belong to one customer agreement. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customer_agreement_id") + private CustomerAgreementEntity customerAgreement; + + /** + * Usage summary associated with this statement. + * Many statements can belong to one usage summary. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "usage_summary_id") + private org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity usageSummary; + @Override public final boolean equals(Object o) { if (this == o) return true; @@ -88,10 +114,13 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + getId() + ", " + - "issueDateTime = " + getIssueDateTime() + ", " + "description = " + getDescription() + ", " + "created = " + getCreated() + ", " + "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + + "issueDateTime = " + getIssueDateTime() + ", " + + "relatedLinks = " + getRelatedLinks() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java index f642dedb..b15f8c02 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java @@ -19,40 +19,28 @@ package org.greenbuttonalliance.espi.common.domain.customer.entity; -import jakarta.persistence.*; -import lombok.Getter; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.proxy.HibernateProxy; -import org.hibernate.type.SqlTypes; +import lombok.ToString; -import java.util.Objects; -import java.util.UUID; +import java.io.Serializable; /** - * Pure JPA/Hibernate entity for StatementRef without JAXB concerns. + * Embeddable class for StatementRef without JAXB concerns. * * [extension] A sequence of references to a document associated with a Statement. * - * Note: StatementRef does NOT extend IdentifiedObject per ESPI 4.0 specification. - * It is not a top-level resource with selfLink/upLink/relatedLinks. + * Note: StatementRef extends Object (not IdentifiedObject) per customer.xsd lines 285-307. + * It is not a top-level resource and has no selfLink/upLink/relatedLinks. + * Stored as @ElementCollection in StatementEntity. */ -@Entity -@Table(name = "statement_refs") -@Getter -@Setter +@Embeddable +@Data @NoArgsConstructor -public class StatementRefEntity { - - /** - * Primary key identifier. - */ - @Id - @GeneratedValue(strategy = GenerationType.UUID) - @JdbcTypeCode(SqlTypes.CHAR) - @Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false) - private UUID id; +@ToString +public class StatementRefEntity implements Serializable { /** * [extension] Name of document or file including filename extension if present. @@ -67,44 +55,9 @@ public class StatementRefEntity { private String mediaType; /** - * [extension] URL used to access a representation of a statement, for example a bill image. + * [extension] URL used to access a representation of a statement, for example a bill image. * Use CDATA or URL encoding to escape characters not allowed in XML. */ @Column(name = "statement_url", length = 2048) private String statementURL; - - /** - * Statement this reference belongs to - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "statement_id") - private StatementEntity statement; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? - hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ? - hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - StatementRefEntity that = (StatementRefEntity) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy hibernateProxy ? - hibernateProxy.getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + - "id = " + getId() + ", " + - "fileName = " + getFileName() + ", " + - "mediaType = " + getMediaType() + ", " + - "statementURL = " + getStatementURL() + ")"; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java index 525a9276..7b989354 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java @@ -19,29 +19,32 @@ 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.time.OffsetDateTime; import java.util.List; /** - * Statement DTO class for JAXB XML marshalling/unmarshalling. + * Statement DTO for JAXB XML marshalling/unmarshalling. + * + * [extension] Billing statement for provided services. + * + * Corresponds to customer.xsd Statement definition (lines 373-393). + * Statement extends IdentifiedObject, but DTO excludes IdentifiedObject fields + * (id, description, published, updated, links) as they're handled by Atom Entry wrapper. * - * Represents a billing statement or document for a customer agreement. - * Supports Atom protocol XML wrapping. + * ESPI 4.0 XSD Sequence (customer.xsd lines 379-392): + * 1. issueDateTime (TimeType) - optional + * 2. statementRef (StatementRef collection) - optional, unbounded */ @XmlRootElement(name = "Statement", namespace = "http://naesb.org/espi/customer") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "Statement", namespace = "http://naesb.org/espi/customer", propOrder = { - "published", "updated", "selfLink", "upLink", "relatedLinks", - "description", "createdDateTime", "lastModifiedDateTime", "revisionNumber", - "subject", "docStatus", "type", "customerAgreement", "statementRefs" + "issueDateTime", + "statementRef" }) @Getter @Setter @@ -49,79 +52,17 @@ @AllArgsConstructor public class StatementDto { - @XmlTransient - private Long id; - - @XmlAttribute(name = "mRID") - private String uuid; - - @XmlElement(name = "published") - private OffsetDateTime published; - - @XmlElement(name = "updated") - private OffsetDateTime updated; - - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - @XmlElementWrapper(name = "links", namespace = "http://www.w3.org/2005/Atom") - private List relatedLinks; - - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto selfLink; - - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto upLink; - - @XmlElement(name = "description") - private String description; - - @XmlElement(name = "createdDateTime") - private OffsetDateTime createdDateTime; - - @XmlElement(name = "lastModifiedDateTime") - private OffsetDateTime lastModifiedDateTime; - - @XmlElement(name = "revisionNumber") - private String revisionNumber; - - @XmlElement(name = "subject") - private String subject; - - @XmlElement(name = "docStatus") - private String docStatus; - - @XmlElement(name = "type") - private String type; - - @XmlElement(name = "CustomerAgreement") - private CustomerAgreementDto customerAgreement; - - @XmlElement(name = "StatementRef") - @XmlElementWrapper(name = "StatementRefs") - private List statementRefs; - - /** - * Minimal constructor for basic statement data. - */ - public StatementDto(String uuid, String subject) { - this(null, uuid, null, null, null, null, null, null, - null, null, null, subject, null, null, null, null); - } - /** - * Gets the self href for this statement. - * - * @return self href string + * [extension] Date and time at which a billing statement was issued. + * Stored as Unix epoch timestamp (seconds since 1970-01-01T00:00:00Z). */ - public String getSelfHref() { - return selfLink != null ? selfLink.getHref() : null; - } + @XmlElement(name = "issueDateTime", namespace = "http://naesb.org/espi/customer") + private Long issueDateTime; /** - * Gets the up href for this statement. - * - * @return up href string + * [extension] Contains document reference metadata needed to access a document + * representation of a billing statement. */ - public String getUpHref() { - return upLink != null ? upLink.getHref() : null; - } -} \ No newline at end of file + @XmlElement(name = "statementRef", namespace = "http://naesb.org/espi/customer") + private List statementRef; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java index 28c3d208..709dfa9c 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java @@ -25,25 +25,25 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.OffsetDateTime; - /** - * StatementRef DTO class for JAXB XML marshalling/unmarshalling. + * StatementRef DTO for JAXB XML marshalling/unmarshalling. * - * Represents a reference to a statement document. + * [extension] A sequence of references to a document associated with a Statement. * - * Note: StatementRef does NOT extend IdentifiedObject per ESPI 4.0 specification. - * It is not a top-level resource with selfLink/upLink/relatedLinks. + * Corresponds to customer.xsd StatementRef definition (lines 285-307). + * StatementRef extends Object (not IdentifiedObject), so it has no id/links/metadata. * - * WARNING: DTO fields do not currently match entity fields. - * Entity has: fileName, mediaType, statementURL - * DTO has: referenceId, referenceType, referenceDate, referenceUrl - * This mismatch needs to be resolved. + * ESPI 4.0 XSD Sequence (customer.xsd lines 291-306): + * 1. fileName (String256) - optional + * 2. mediaType (String256) - optional + * 3. statementURL (String2048) - optional */ @XmlRootElement(name = "StatementRef", namespace = "http://naesb.org/espi/customer") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "StatementRef", namespace = "http://naesb.org/espi/customer", propOrder = { - "referenceId", "referenceType", "referenceDate", "referenceUrl", "statement" + "fileName", + "mediaType", + "statementURL" }) @Getter @Setter @@ -51,25 +51,23 @@ @AllArgsConstructor public class StatementRefDto { - @XmlElement(name = "referenceId") - private String referenceId; - - @XmlElement(name = "referenceType") - private String referenceType; - - @XmlElement(name = "referenceDate") - private OffsetDateTime referenceDate; - - @XmlElement(name = "referenceUrl") - private String referenceUrl; + /** + * [extension] Name of document or file including filename extension if present. + */ + @XmlElement(name = "fileName", namespace = "http://naesb.org/espi/customer") + private String fileName; - @XmlElement(name = "Statement") - private StatementDto statement; + /** + * [extension] Document media type as published by IANA. + * See https://www.iana.org/assignments/media-types for more information. + */ + @XmlElement(name = "mediaType", namespace = "http://naesb.org/espi/customer") + private String mediaType; /** - * Minimal constructor for basic reference data. + * [extension] URL used to access a representation of a statement, for example a bill image. + * Use CDATA or URL encoding to escape characters not allowed in XML. */ - public StatementRefDto(String referenceId, String referenceUrl) { - this(referenceId, null, null, referenceUrl, null); - } -} \ No newline at end of file + @XmlElement(name = "statementURL", namespace = "http://naesb.org/espi/customer") + private String statementURL; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementMapper.java new file mode 100644 index 00000000..1c24164e --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementMapper.java @@ -0,0 +1,68 @@ +/* + * + * 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.StatementEntity; +import org.greenbuttonalliance.espi.common.dto.customer.StatementDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for converting between StatementEntity and StatementDto. + * + * Maps ONLY customer.xsd Statement fields (lines 373-393): + * - issueDateTime (requires DateTimeMapper conversion) + * - statementRef collection (requires StatementRefMapper) + * + * All Atom metadata (id, description, published, updated, links) handled by + * AtomEntryDto and LinkDto per DRY principle. + */ +@Mapper(componentModel = "spring", uses = {DateTimeMapper.class, StatementRefMapper.class}) +public interface StatementMapper { + + /** + * Maps StatementEntity to StatementDto for XML export. + * + * Field mappings: + * - issueDateTime: OffsetDateTime → Long (via DateTimeMapper.mapToLong) + * - statementRefs: List → List (via StatementRefMapper) + * + * @param entity the StatementEntity to map + * @return the mapped StatementDto, or null if entity is null + */ + @Mapping(target = "issueDateTime", source = "issueDateTime", qualifiedByName = "offsetToLong") + @Mapping(target = "statementRef", source = "statementRefs") + StatementDto toDto(StatementEntity entity); + + /** + * Maps StatementDto to StatementEntity for import. + * + * Field mappings: + * - issueDateTime: Long → OffsetDateTime (via DateTimeMapper.mapFromLong) + * - statementRef: List → List (via StatementRefMapper) + * + * @param dto the StatementDto to map + * @return the mapped StatementEntity, or null if dto is null + */ + @Mapping(target = "issueDateTime", source = "issueDateTime", qualifiedByName = "longToOffset") + @Mapping(target = "statementRefs", source = "statementRef") + StatementEntity toEntity(StatementDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementRefMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementRefMapper.java new file mode 100644 index 00000000..950f4362 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StatementRefMapper.java @@ -0,0 +1,55 @@ +/* + * + * 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.StatementRefEntity; +import org.greenbuttonalliance.espi.common.dto.customer.StatementRefDto; +import org.mapstruct.Mapper; + +/** + * MapStruct mapper for converting between StatementRefEntity and StatementRefDto. + * + * StatementRef is a simple embeddable with 3 fields (fileName, mediaType, statementURL). + * Direct field mapping - no transformations needed. + * + * Per customer.xsd lines 285-307: + * - StatementRef extends Object (not IdentifiedObject) + * - No id, links, or metadata fields + * - Simple 1:1 field mapping + */ +@Mapper(componentModel = "spring") +public interface StatementRefMapper { + + /** + * Maps StatementRefEntity to StatementRefDto for XML export. + * + * @param entity the StatementRefEntity to map + * @return the mapped StatementRefDto, or null if entity is null + */ + StatementRefDto toDto(StatementRefEntity entity); + + /** + * Maps StatementRefDto to StatementRefEntity for import. + * + * @param dto the StatementRefDto to map + * @return the mapped StatementRefEntity, or null if dto is null + */ + StatementRefEntity toEntity(StatementRefDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepository.java index adae2ca3..410fa4cd 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepository.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepository.java @@ -21,61 +21,56 @@ import org.greenbuttonalliance.espi.common.domain.customer.entity.StatementEntity; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; /** * Spring Data JPA repository for Statement entities. - * - * Manages billing statement data with document references and issue dates. + * + * [extension] Billing statement for provided services. + * + * Provides standard CRUD operations via JpaRepository and ID-based relationship queries. + * Per ESPI 4.0 compliance (Issue #28 Phase 19), non-ID queries removed to prevent + * performance issues and ensure consistent API patterns. */ @Repository public interface StatementRepository extends JpaRepository { /** - * Find statements issued after specified date. - */ - @Query("SELECT s FROM StatementEntity s WHERE s.issueDateTime > :dateTime") - List findByIssueDateTimeAfter(@Param("dateTime") OffsetDateTime dateTime); - - /** - * Find statements issued before specified date. - */ - @Query("SELECT s FROM StatementEntity s WHERE s.issueDateTime < :dateTime") - List findByIssueDateTimeBefore(@Param("dateTime") OffsetDateTime dateTime); - - /** - * Find statements issued between specified dates. - */ - @Query("SELECT s FROM StatementEntity s WHERE s.issueDateTime BETWEEN :startDate AND :endDate") - List findByIssueDateTimeBetween(@Param("startDate") OffsetDateTime startDate, @Param("endDate") OffsetDateTime endDate); - - /** - * Find statements with document references. + * Find statements by customer ID. + * Supports Controller APIs for navigating Statement → Customer relationships. + * + * @param customerId the customer UUID + * @return list of statements for the customer */ - @Query("SELECT s FROM StatementEntity s WHERE SIZE(s.statementRefs) > 0") - List findStatementsWithReferences(); + List findByCustomerId(UUID customerId); /** - * Find statements without document references. + * Find statements by customer account ID. + * Supports Controller APIs for navigating Statement → CustomerAccount relationships. + * + * @param customerAccountId the customer account UUID + * @return list of statements for the customer account */ - @Query("SELECT s FROM StatementEntity s WHERE SIZE(s.statementRefs) = 0") - List findStatementsWithoutReferences(); + List findByCustomerAccountId(UUID customerAccountId); /** - * Find statements by description (from IdentifiedObject base class). + * Find statements by customer agreement ID. + * Supports Controller APIs for navigating Statement → CustomerAgreement relationships. + * + * @param customerAgreementId the customer agreement UUID + * @return list of statements for the customer agreement */ - @Query("SELECT s FROM StatementEntity s WHERE UPPER(s.description) LIKE UPPER(CONCAT('%', :description, '%'))") - List findByDescriptionContaining(@Param("description") String description); + List findByCustomerAgreementId(UUID customerAgreementId); /** - * Find recent statements (issued within last N days). + * Find statements by usage summary ID. + * Supports Controller APIs for navigating Statement → UsageSummary relationships. + * + * @param usageSummaryId the usage summary UUID + * @return list of statements for the usage summary */ - @Query("SELECT s FROM StatementEntity s WHERE s.issueDateTime > :cutoffDate ORDER BY s.issueDateTime DESC") - List findRecentStatements(@Param("cutoffDate") OffsetDateTime cutoffDate); -} \ No newline at end of file + List findByUsageSummaryId(UUID usageSummaryId); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/StatementService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/StatementService.java index 2d7b6cca..337246e8 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/StatementService.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/StatementService.java @@ -21,92 +21,80 @@ import org.greenbuttonalliance.espi.common.domain.customer.entity.StatementEntity; -import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; /** * Service interface for Statement management. - * - * Handles business logic for billing statement operations including bill amounts, - * due dates, payment tracking, and statement status management. + * + * [extension] Billing statement for provided services. + * + * Provides standard CRUD operations and ID-based relationship queries. + * Per ESPI 4.0 compliance (Issue #28 Phase 19), non-ID queries removed to prevent + * performance issues and ensure consistent API patterns. */ public interface StatementService { /** * Find all statements. + * + * @return list of all statements */ List findAll(); /** * Find statement by ID. + * + * @param id the statement UUID + * @return Optional containing the statement if found */ Optional findById(UUID id); - - /** - * Find statements issued after specified date. - */ - List findByIssueDateTimeAfter(OffsetDateTime dateTime); - - /** - * Find statements issued before specified date. - */ - List findByIssueDateTimeBefore(OffsetDateTime dateTime); - - /** - * Find statements issued between specified dates. - */ - List findByIssueDateTimeBetween(OffsetDateTime startDate, OffsetDateTime endDate); - /** - * Find statements with document references. + * Find statements by customer ID. + * + * @param customerId the customer UUID + * @return list of statements for the customer */ - List findStatementsWithReferences(); + List findByCustomerId(UUID customerId); /** - * Find statements without document references. + * Find statements by customer account ID. + * + * @param customerAccountId the customer account UUID + * @return list of statements for the customer account */ - List findStatementsWithoutReferences(); + List findByCustomerAccountId(UUID customerAccountId); /** - * Find statements by description containing text. + * Find statements by customer agreement ID. + * + * @param customerAgreementId the customer agreement UUID + * @return list of statements for the customer agreement */ - List findByDescriptionContaining(String description); + List findByCustomerAgreementId(UUID customerAgreementId); /** - * Find recent statements (issued within last N days). + * Find statements by usage summary ID. + * + * @param usageSummaryId the usage summary UUID + * @return list of statements for the usage summary */ - List findRecentStatements(OffsetDateTime cutoffDate); + List findByUsageSummaryId(UUID usageSummaryId); /** * Save statement. + * + * @param statement the statement to save + * @return the saved statement */ StatementEntity save(StatementEntity statement); /** * Delete statement by ID. + * + * @param id the statement UUID to delete */ void deleteById(UUID id); - - /** - * Update statement description. - */ - StatementEntity updateDescription(UUID id, String description); - - /** - * Count total statements. - */ - long countStatements(); - - /** - * Count statements with references. - */ - long countStatementsWithReferences(); - - /** - * Count statements without references. - */ - long countStatementsWithoutReferences(); -} \ No newline at end of file +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/StatementServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/StatementServiceImpl.java index 8822dc0d..65770fec 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/StatementServiceImpl.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/StatementServiceImpl.java @@ -26,16 +26,18 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; /** * Service implementation for Statement management. - *

- * Provides business logic for billing statement operations including bill amounts, - * due dates, payment tracking, and statement status management. + * + * [extension] Billing statement for provided services. + * + * Provides standard CRUD operations and ID-based relationship queries. + * Per ESPI 4.0 compliance (Issue #28 Phase 19), non-ID queries removed to prevent + * performance issues and ensure consistent API patterns. */ @Service @Transactional @@ -58,52 +60,30 @@ public Optional findById(UUID id) { @Override @Transactional(readOnly = true) - public List findByIssueDateTimeAfter(OffsetDateTime dateTime) { - return statementRepository.findByIssueDateTimeAfter(dateTime); - } - - @Override - @Transactional(readOnly = true) - public List findByIssueDateTimeBefore(OffsetDateTime dateTime) { - return statementRepository.findByIssueDateTimeBefore(dateTime); + public List findByCustomerId(UUID customerId) { + return statementRepository.findByCustomerId(customerId); } @Override @Transactional(readOnly = true) - public List findByIssueDateTimeBetween(OffsetDateTime startDate, OffsetDateTime endDate) { - return statementRepository.findByIssueDateTimeBetween(startDate, endDate); + public List findByCustomerAccountId(UUID customerAccountId) { + return statementRepository.findByCustomerAccountId(customerAccountId); } @Override @Transactional(readOnly = true) - public List findStatementsWithReferences() { - return statementRepository.findStatementsWithReferences(); + public List findByCustomerAgreementId(UUID customerAgreementId) { + return statementRepository.findByCustomerAgreementId(customerAgreementId); } @Override @Transactional(readOnly = true) - public List findStatementsWithoutReferences() { - return statementRepository.findStatementsWithoutReferences(); - } - - @Override - @Transactional(readOnly = true) - public List findByDescriptionContaining(String description) { - return statementRepository.findByDescriptionContaining(description); - } - - @Override - @Transactional(readOnly = true) - public List findRecentStatements(OffsetDateTime cutoffDate) { - return statementRepository.findRecentStatements(cutoffDate); + public List findByUsageSummaryId(UUID usageSummaryId) { + return statementRepository.findByUsageSummaryId(usageSummaryId); } @Override public StatementEntity save(StatementEntity statement) { - // Generate UUID if not present - if (statement.getId() == null) { - statement.setId(UUID.randomUUID()); - } return statementRepository.save(statement); } @@ -111,33 +91,4 @@ public StatementEntity save(StatementEntity statement) { public void deleteById(UUID id) { statementRepository.deleteById(id); } - - @Override - public StatementEntity updateDescription(UUID id, String description) { - Optional optionalStatement = statementRepository.findById(id); - if (optionalStatement.isPresent()) { - StatementEntity statement = optionalStatement.get(); - statement.setDescription(description); - return statementRepository.save(statement); - } - throw new IllegalArgumentException("Statement not found with id: " + id); - } - - @Override - @Transactional(readOnly = true) - public long countStatements() { - return statementRepository.count(); - } - - @Override - @Transactional(readOnly = true) - public long countStatementsWithReferences() { - return statementRepository.findStatementsWithReferences().size(); - } - - @Override - @Transactional(readOnly = true) - public long countStatementsWithoutReferences() { - return statementRepository.findStatementsWithoutReferences().size(); - } -} \ No newline at end of file +} 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 b96ed008..ebe3d353 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 @@ -916,19 +916,26 @@ CREATE TABLE statements self_link_href VARCHAR(1024), self_link_type VARCHAR(255), - -- Statement specific fields + -- Statement specific fields (customer.xsd lines 373-393) + -- Statement has: issueDateTime, statementRef (collection in statement_refs table) issue_date_time TIMESTAMP, + + -- Foreign keys for bidirectional relationships (JPA navigation for Controller APIs) customer_id CHAR(36), - statement_date BIGINT, - billing_period_start BIGINT, - billing_period_duration BIGINT, - FOREIGN KEY (customer_id) REFERENCES customers(id) + customer_account_id CHAR(36), + customer_agreement_id CHAR(36), + usage_summary_id CHAR(36), + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL, + FOREIGN KEY (customer_account_id) REFERENCES customer_accounts(id) ON DELETE SET NULL, + FOREIGN KEY (customer_agreement_id) REFERENCES customer_agreements(id) ON DELETE SET NULL, + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries(id) ON DELETE SET NULL ); -CREATE INDEX idx_statement_issue_date_time ON statements (issue_date_time); +-- Indexes for statements table (ID-based per ESPI standard) CREATE INDEX idx_statement_customer_id ON statements (customer_id); -CREATE INDEX idx_statement_statement_date ON statements (statement_date); -CREATE INDEX idx_statement_billing_period_start ON statements (billing_period_start); +CREATE INDEX idx_statement_customer_account_id ON statements (customer_account_id); +CREATE INDEX idx_statement_customer_agreement_id ON statements (customer_agreement_id); +CREATE INDEX idx_statement_usage_summary_id ON statements (usage_summary_id); CREATE INDEX idx_statement_created ON statements (created); CREATE INDEX idx_statement_updated ON statements (updated); @@ -944,29 +951,16 @@ CREATE TABLE statement_related_links CREATE INDEX idx_statement_related_links ON statement_related_links (statement_id); --- Statement Ref Table +-- Statement Refs Collection Table +-- StatementRef extends Object (not IdentifiedObject) per customer.xsd lines 285-307 +-- Stored as @ElementCollection in StatementEntity - no id column needed CREATE TABLE statement_refs ( - id CHAR(36) PRIMARY KEY , - description VARCHAR(255), - created TIMESTAMP, - updated TIMESTAMP, - published TIMESTAMP, - up_link_rel VARCHAR(255), - up_link_href VARCHAR(1024), - up_link_type VARCHAR(255), - self_link_rel VARCHAR(255), - self_link_href VARCHAR(1024), - self_link_type VARCHAR(255), - - -- Statement ref specific fields - file_name VARCHAR(512), - media_type VARCHAR(256), - statement_url VARCHAR(2048), - statement_id CHAR(36), - FOREIGN KEY (statement_id) REFERENCES statements(id) + statement_id CHAR(36) NOT NULL, + file_name VARCHAR(512), + media_type VARCHAR(256), + statement_url VARCHAR(2048), + FOREIGN KEY (statement_id) REFERENCES statements(id) ON DELETE CASCADE ); -CREATE INDEX idx_statement_ref_statement_id ON statement_refs (statement_id); -CREATE INDEX idx_statement_ref_created ON statement_refs (created); -CREATE INDEX idx_statement_ref_updated ON statement_refs (updated); \ No newline at end of file +CREATE INDEX idx_statement_ref_statement_id ON statement_refs (statement_id); \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepositoryTest.java index e12268b3..eebb5a4a 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/StatementRepositoryTest.java @@ -18,33 +18,28 @@ package org.greenbuttonalliance.espi.common.repositories.customer; -import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity; -import org.greenbuttonalliance.espi.common.domain.customer.entity.StatementEntity; -import org.greenbuttonalliance.espi.common.domain.customer.entity.StatementRefEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.*; +import org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity; +import org.greenbuttonalliance.espi.common.repositories.usage.UsageSummaryRepository; import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest; -import org.greenbuttonalliance.espi.common.test.TestDataBuilders; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import jakarta.validation.ConstraintViolation; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; /** - * Comprehensive test suite for StatementRepository. - * - * Tests all CRUD operations, custom query methods, relationships, - * and validation constraints for Statement entities. + * Test suite for StatementRepository - Phase 19 ESPI 4.0 Compliance. + * + * Tests CRUD operations and ID-based relationship queries following + * ESPI standard patterns. Non-ID queries removed per Issue #28. */ -@DisplayName("Statement Repository Tests") +@DisplayName("Statement Repository Tests - Phase 19") class StatementRepositoryTest extends BaseRepositoryTest { @Autowired @@ -53,486 +48,196 @@ class StatementRepositoryTest extends BaseRepositoryTest { @Autowired private CustomerRepository customerRepository; - @Nested - @DisplayName("CRUD Operations") - class CrudOperationsTest { - - @Test - @DisplayName("Should save and retrieve statement successfully") - void shouldSaveAndRetrieveStatementSuccessfully() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription("Test Statement for CRUD"); - - // Act - StatementEntity saved = statementRepository.save(statement); - flushAndClear(); - Optional retrieved = statementRepository.findById(saved.getId()); - - // Assert - assertThat(saved).isNotNull(); - assertThat(saved.getId()).isNotNull(); - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getDescription()).isEqualTo("Test Statement for CRUD"); - assertThat(retrieved.get().getIssueDateTime()).isEqualTo(statement.getIssueDateTime()); - } - - @Test - @DisplayName("Should save statement with customer relationship") - void shouldSaveStatementWithCustomerRelationship() { - // Arrange - CustomerEntity customer = TestDataBuilders.createValidCustomer(); - customer.setCustomerName("Test Customer for Statement"); - CustomerEntity savedCustomer = customerRepository.save(customer); - - StatementEntity statement = TestDataBuilders.createValidStatementWithCustomer(savedCustomer); - statement.setDescription("Statement with Customer"); - - // Act - StatementEntity saved = statementRepository.save(statement); - flushAndClear(); - Optional retrieved = statementRepository.findById(saved.getId()); - - // Assert - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getCustomer()).isNotNull(); - assertThat(retrieved.get().getCustomer().getId()).isEqualTo(savedCustomer.getId()); - assertThat(retrieved.get().getCustomer().getCustomerName()).isEqualTo("Test Customer for Statement"); - } - - @Test - @DisplayName("Should find all statements") - void shouldFindAllStatements() { - // Arrange - List statements = TestDataBuilders.createValidEntities(3, TestDataBuilders::createValidStatement); - statements.forEach(s -> s.setDescription("Bulk Statement " + statements.indexOf(s))); - statementRepository.saveAll(statements); - flushAndClear(); - - // Act - List allStatements = statementRepository.findAll(); - - // Assert - assertThat(allStatements).hasSizeGreaterThanOrEqualTo(3); - assertThat(allStatements).extracting(StatementEntity::getDescription) - .contains("Bulk Statement 0", "Bulk Statement 1", "Bulk Statement 2"); - } - - @Test - @DisplayName("Should delete statement successfully") - void shouldDeleteStatementSuccessfully() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription("Statement to Delete"); - StatementEntity saved = statementRepository.save(statement); - UUID statementId = saved.getId(); - flushAndClear(); - - // Act - statementRepository.deleteById(statementId); - flushAndClear(); - Optional retrieved = statementRepository.findById(statementId); - - // Assert - assertThat(retrieved).isEmpty(); - } - - @Test - @DisplayName("Should check if statement exists") - void shouldCheckIfStatementExists() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - StatementEntity saved = statementRepository.save(statement); - flushAndClear(); - - // Act & Assert - assertThat(statementRepository.existsById(saved.getId())).isTrue(); - assertThat(statementRepository.existsById(UUID.randomUUID())).isFalse(); - } - - @Test - @DisplayName("Should count statements") - void shouldCountStatements() { - // Arrange - long initialCount = statementRepository.count(); - List statements = TestDataBuilders.createValidEntities(5, TestDataBuilders::createValidStatement); - statementRepository.saveAll(statements); - flushAndClear(); - - // Act - long finalCount = statementRepository.count(); - - // Assert - assertThat(finalCount).isEqualTo(initialCount + 5); - } + @Autowired + private CustomerAccountRepository customerAccountRepository; + + @Autowired + private CustomerAgreementRepository customerAgreementRepository; + + @Autowired + private UsageSummaryRepository usageSummaryRepository; + + @Test + @DisplayName("Should save and retrieve statement successfully") + void shouldSaveAndRetrieveStatement() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("Test Statement"); + // Truncate to microseconds (6 decimal places) for cross-platform compatibility (Windows limit) + OffsetDateTime now = OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS); + statement.setIssueDateTime(now); + + // Act + StatementEntity saved = statementRepository.save(statement); + flushAndClear(); + Optional retrieved = statementRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(stmt -> assertThat(stmt) + .extracting("description", "issueDateTime") + .containsExactly("Test Statement", now)); + } + + @Test + @DisplayName("Should save statement with StatementRef collection") + void shouldSaveStatementWithRefs() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement with Refs"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + + // StatementRef is @Embeddable, stored as @ElementCollection + List refs = new ArrayList<>(); + StatementRefEntity ref1 = new StatementRefEntity(); + ref1.setFileName("bill.pdf"); + ref1.setMediaType("application/pdf"); + ref1.setStatementURL("https://example.com/bills/bill.pdf"); + refs.add(ref1); + + StatementRefEntity ref2 = new StatementRefEntity(); + ref2.setFileName("invoice.html"); + ref2.setMediaType("text/html"); + ref2.setStatementURL("https://example.com/bills/invoice.html"); + refs.add(ref2); + + statement.setStatementRefs(refs); + + // Act + StatementEntity saved = statementRepository.save(statement); + flushAndClear(); + Optional retrieved = statementRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(stmt -> { + assertThat(stmt.getStatementRefs()).hasSize(2); + assertThat(stmt.getStatementRefs().get(0)) + .extracting("fileName", "mediaType") + .containsExactly("bill.pdf", "application/pdf"); + }); + } + + @Test + @DisplayName("Should find statements by customer ID") + void shouldFindByCustomerId() { + // Arrange + CustomerEntity customer = new CustomerEntity(); + customer.setCustomerName("Test Customer"); + CustomerEntity savedCustomer = customerRepository.save(customer); + + StatementEntity statement1 = createStatementWithCustomer(savedCustomer); + StatementEntity statement2 = createStatementWithCustomer(savedCustomer); + statementRepository.save(statement1); + statementRepository.save(statement2); + flushAndClear(); + + // Act + List found = statementRepository.findByCustomerId(savedCustomer.getId()); + + // Assert + assertThat(found).hasSize(2) + .allSatisfy(stmt -> assertThat(stmt.getCustomer().getId()) + .isEqualTo(savedCustomer.getId())); + } + + @Test + @DisplayName("Should find statements by customer account ID") + void shouldFindByCustomerAccountId() { + // Arrange + CustomerAccountEntity account = new CustomerAccountEntity(); + account.setDescription("Test Account"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Account"); + statement.setCustomerAccount(savedAccount); + statementRepository.save(statement); + flushAndClear(); + + // Act + List found = statementRepository.findByCustomerAccountId(savedAccount.getId()); + + // Assert + assertThat(found).hasSize(1) + .first() + .extracting(s -> s.getCustomerAccount().getId()) + .isEqualTo(savedAccount.getId()); } - @Nested - @DisplayName("Custom Query Methods") - class CustomQueryMethodsTest { - - @Test - @DisplayName("Should find statements issued after specified date") - void shouldFindStatementsIssuedAfterSpecifiedDate() { - // Arrange - OffsetDateTime cutoffDate = OffsetDateTime.now().minusDays(15); - OffsetDateTime recentDate = OffsetDateTime.now().minusDays(5); - OffsetDateTime oldDate = OffsetDateTime.now().minusDays(25); - - StatementEntity recentStatement = TestDataBuilders.createValidStatement(); - recentStatement.setIssueDateTime(recentDate); - recentStatement.setDescription("Recent Statement"); - - StatementEntity oldStatement = TestDataBuilders.createValidStatement(); - oldStatement.setIssueDateTime(oldDate); - oldStatement.setDescription("Old Statement"); - - statementRepository.saveAll(List.of(recentStatement, oldStatement)); - flushAndClear(); - - // Act - List results = statementRepository.findByIssueDateTimeAfter(cutoffDate); - - // Assert - assertThat(results).isNotEmpty(); - assertThat(results).extracting(StatementEntity::getDescription).contains("Recent Statement"); - assertThat(results).extracting(StatementEntity::getDescription).doesNotContain("Old Statement"); - assertThat(results).allMatch(s -> s.getIssueDateTime().isAfter(cutoffDate)); - } - - @Test - @DisplayName("Should find statements issued before specified date") - void shouldFindStatementsIssuedBeforeSpecifiedDate() { - // Arrange - OffsetDateTime cutoffDate = OffsetDateTime.now().minusDays(15); - OffsetDateTime recentDate = OffsetDateTime.now().minusDays(5); - OffsetDateTime oldDate = OffsetDateTime.now().minusDays(25); - - StatementEntity recentStatement = TestDataBuilders.createValidStatement(); - recentStatement.setIssueDateTime(recentDate); - recentStatement.setDescription("Recent Statement"); - - StatementEntity oldStatement = TestDataBuilders.createValidStatement(); - oldStatement.setIssueDateTime(oldDate); - oldStatement.setDescription("Old Statement"); - - statementRepository.saveAll(List.of(recentStatement, oldStatement)); - flushAndClear(); - - // Act - List results = statementRepository.findByIssueDateTimeBefore(cutoffDate); - - // Assert - assertThat(results).isNotEmpty(); - assertThat(results).extracting(StatementEntity::getDescription).contains("Old Statement"); - assertThat(results).extracting(StatementEntity::getDescription).doesNotContain("Recent Statement"); - assertThat(results).allMatch(s -> s.getIssueDateTime().isBefore(cutoffDate)); - } - - @Test - @DisplayName("Should find statements issued between specified dates") - void shouldFindStatementsIssuedBetweenSpecifiedDates() { - // Arrange - OffsetDateTime startDate = OffsetDateTime.now().minusDays(20); - OffsetDateTime endDate = OffsetDateTime.now().minusDays(10); - - OffsetDateTime withinRangeDate = OffsetDateTime.now().minusDays(15); - OffsetDateTime beforeRangeDate = OffsetDateTime.now().minusDays(25); - OffsetDateTime afterRangeDate = OffsetDateTime.now().minusDays(5); - - StatementEntity withinRangeStatement = TestDataBuilders.createValidStatement(); - withinRangeStatement.setIssueDateTime(withinRangeDate); - withinRangeStatement.setDescription("Within Range Statement"); - - StatementEntity beforeRangeStatement = TestDataBuilders.createValidStatement(); - beforeRangeStatement.setIssueDateTime(beforeRangeDate); - beforeRangeStatement.setDescription("Before Range Statement"); - - StatementEntity afterRangeStatement = TestDataBuilders.createValidStatement(); - afterRangeStatement.setIssueDateTime(afterRangeDate); - afterRangeStatement.setDescription("After Range Statement"); - - statementRepository.saveAll(List.of(withinRangeStatement, beforeRangeStatement, afterRangeStatement)); - flushAndClear(); - - // Act - List results = statementRepository.findByIssueDateTimeBetween(startDate, endDate); - - // Assert - assertThat(results).isNotEmpty(); - assertThat(results).extracting(StatementEntity::getDescription).contains("Within Range Statement"); - assertThat(results).extracting(StatementEntity::getDescription) - .doesNotContain("Before Range Statement", "After Range Statement"); - assertThat(results).allMatch(s -> - s.getIssueDateTime().isAfter(startDate) && s.getIssueDateTime().isBefore(endDate)); - } - - @Test - @DisplayName("Should find statements with references") - void shouldFindStatementsWithReferences() { - // Arrange - StatementEntity statementWithRefs = TestDataBuilders.createValidStatement(); - statementWithRefs.setDescription("Statement With References"); - statementWithRefs.setStatementRefs(new ArrayList<>()); - - // Create mock statement references - StatementRefEntity ref1 = new StatementRefEntity(); - ref1.setStatement(statementWithRefs); - StatementRefEntity ref2 = new StatementRefEntity(); - ref2.setStatement(statementWithRefs); - - statementWithRefs.getStatementRefs().add(ref1); - statementWithRefs.getStatementRefs().add(ref2); - - StatementEntity statementWithoutRefs = TestDataBuilders.createValidStatement(); - statementWithoutRefs.setDescription("Statement Without References"); - statementWithoutRefs.setStatementRefs(new ArrayList<>()); - - statementRepository.saveAll(List.of(statementWithRefs, statementWithoutRefs)); - flushAndClear(); - - // Act - List results = statementRepository.findStatementsWithReferences(); - - // Assert - assertThat(results).isNotEmpty(); - assertThat(results).extracting(StatementEntity::getDescription).contains("Statement With References"); - assertThat(results).extracting(StatementEntity::getDescription).doesNotContain("Statement Without References"); - assertThat(results).allMatch(s -> s.getStatementRefs() != null && !s.getStatementRefs().isEmpty()); - } - - @Test - @DisplayName("Should find statements without references") - void shouldFindStatementsWithoutReferences() { - // Arrange - StatementEntity statementWithoutRefs = TestDataBuilders.createValidStatement(); - statementWithoutRefs.setDescription("Statement Without References"); - statementWithoutRefs.setStatementRefs(new ArrayList<>()); - - StatementEntity statementWithRefs = TestDataBuilders.createValidStatement(); - statementWithRefs.setDescription("Statement With References"); - statementWithRefs.setStatementRefs(new ArrayList<>()); - - StatementRefEntity ref = new StatementRefEntity(); - ref.setStatement(statementWithRefs); - statementWithRefs.getStatementRefs().add(ref); - - statementRepository.saveAll(List.of(statementWithoutRefs, statementWithRefs)); - flushAndClear(); - - // Act - List results = statementRepository.findStatementsWithoutReferences(); - - // Assert - assertThat(results).isNotEmpty(); - assertThat(results).extracting(StatementEntity::getDescription).contains("Statement Without References"); - assertThat(results).extracting(StatementEntity::getDescription).doesNotContain("Statement With References"); - assertThat(results).allMatch(s -> s.getStatementRefs() == null || s.getStatementRefs().isEmpty()); - } - - @Test - @DisplayName("Should find statements by description containing text") - void shouldFindStatementsByDescriptionContainingText() { - // Arrange - StatementEntity statement1 = TestDataBuilders.createValidStatement(); - statement1.setDescription("Monthly Electricity Bill Statement"); - - StatementEntity statement2 = TestDataBuilders.createValidStatement(); - statement2.setDescription("Annual Gas Usage Report"); - - StatementEntity statement3 = TestDataBuilders.createValidStatement(); - statement3.setDescription("Quarterly Electricity Summary"); - - statementRepository.saveAll(List.of(statement1, statement2, statement3)); - flushAndClear(); - - // Act - List results = statementRepository.findByDescriptionContaining("electricity"); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(StatementEntity::getDescription) - .contains("Monthly Electricity Bill Statement", "Quarterly Electricity Summary"); - assertThat(results).extracting(StatementEntity::getDescription) - .doesNotContain("Annual Gas Usage Report"); - } - - @Test - @DisplayName("Should find recent statements") - void shouldFindRecentStatements() { - // Arrange - OffsetDateTime cutoffDate = OffsetDateTime.now().minusDays(10); - - StatementEntity recentStatement1 = TestDataBuilders.createValidStatement(); - recentStatement1.setIssueDateTime(OffsetDateTime.now().minusDays(5)); - recentStatement1.setDescription("Recent Statement 1"); - - StatementEntity recentStatement2 = TestDataBuilders.createValidStatement(); - recentStatement2.setIssueDateTime(OffsetDateTime.now().minusDays(3)); - recentStatement2.setDescription("Recent Statement 2"); - - StatementEntity oldStatement = TestDataBuilders.createValidStatement(); - oldStatement.setIssueDateTime(OffsetDateTime.now().minusDays(15)); - oldStatement.setDescription("Old Statement"); - - statementRepository.saveAll(List.of(recentStatement1, recentStatement2, oldStatement)); - flushAndClear(); - - // Act - List results = statementRepository.findRecentStatements(cutoffDate); - - // Assert - assertThat(results).hasSize(2); - assertThat(results).extracting(StatementEntity::getDescription) - .contains("Recent Statement 1", "Recent Statement 2"); - assertThat(results).extracting(StatementEntity::getDescription) - .doesNotContain("Old Statement"); - - // Should be ordered by issue date descending (most recent first) - assertThat(results.get(0).getDescription()).isEqualTo("Recent Statement 2"); - assertThat(results.get(1).getDescription()).isEqualTo("Recent Statement 1"); - } - - @Test - @DisplayName("Should handle empty results gracefully") - void shouldHandleEmptyResultsGracefully() { - // Arrange - OffsetDateTime futureDate = OffsetDateTime.now().plusDays(30); - - // Act - List results = statementRepository.findByIssueDateTimeAfter(futureDate); - - // Assert - assertThat(results).isEmpty(); - } + @Test + @DisplayName("Should find statements by customer agreement ID") + void shouldFindByCustomerAgreementId() { + // Arrange + CustomerAgreementEntity agreement = new CustomerAgreementEntity(); + agreement.setDescription("Test Agreement"); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Agreement"); + statement.setCustomerAgreement(savedAgreement); + statementRepository.save(statement); + flushAndClear(); + + // Act + List found = statementRepository.findByCustomerAgreementId(savedAgreement.getId()); + + // Assert + assertThat(found).hasSize(1) + .first() + .extracting(s -> s.getCustomerAgreement().getId()) + .isEqualTo(savedAgreement.getId()); } - @Nested - @DisplayName("JPA Relationships") - class RelationshipsTest { - - @Test - @DisplayName("Should maintain customer relationship integrity") - void shouldMaintainCustomerRelationshipIntegrity() { - // Arrange - CustomerEntity customer = TestDataBuilders.createValidCustomer(); - customer.setCustomerName("Relationship Test Customer"); - CustomerEntity savedCustomer = customerRepository.save(customer); - - StatementEntity statement = TestDataBuilders.createValidStatementWithCustomer(savedCustomer); - statement.setDescription("Statement with Customer Relationship"); - - // Act - StatementEntity savedStatement = statementRepository.save(statement); - flushAndClear(); - - Optional retrieved = statementRepository.findById(savedStatement.getId()); - - // Assert - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getCustomer()).isNotNull(); - assertThat(retrieved.get().getCustomer().getId()).isEqualTo(savedCustomer.getId()); - assertThat(retrieved.get().getCustomer().getCustomerName()).isEqualTo("Relationship Test Customer"); - } - - @Test - @DisplayName("Should handle null customer relationship") - void shouldHandleNullCustomerRelationship() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription("Statement without Customer"); - statement.setCustomer(null); - - // Act & Assert - assertThatCode(() -> { - StatementEntity saved = statementRepository.save(statement); - flushAndClear(); - Optional retrieved = statementRepository.findById(saved.getId()); - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getCustomer()).isNull(); - }).doesNotThrowAnyException(); - } + @Test + @DisplayName("Should find statements by usage summary ID") + void shouldFindByUsageSummaryId() { + // Arrange + UsageSummaryEntity summary = new UsageSummaryEntity(); + summary.setDescription("Test Summary"); + UsageSummaryEntity savedSummary = usageSummaryRepository.save(summary); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Summary"); + statement.setUsageSummary(savedSummary); + statementRepository.save(statement); + flushAndClear(); + + // Act + List found = statementRepository.findByUsageSummaryId(savedSummary.getId()); + + // Assert + assertThat(found).hasSize(1) + .first() + .extracting(s -> s.getUsageSummary().getId()) + .isEqualTo(savedSummary.getId()); } - @Nested - @DisplayName("Entity Validation") - class ValidationTest { - - @Test - @DisplayName("Should validate statement with valid data") - void shouldValidateStatementWithValidData() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription("Valid Statement Description"); - - // Act - Set> violations = validator.validate(statement); - - // Assert - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("Should handle null issue date time") - void shouldHandleNullIssueDateTime() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setIssueDateTime(null); - statement.setDescription("Statement with null issue date"); - - // Act - Set> violations = validator.validate(statement); - - // Assert - Issue date time is typically optional - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("Should handle null description") - void shouldHandleNullDescription() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription(null); - - // Act - Set> violations = validator.validate(statement); - - // Assert - Description is typically optional in ESPI entities - assertThat(violations).isEmpty(); - } + @Test + @DisplayName("Should delete statement successfully") + void shouldDeleteStatement() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement to Delete"); + StatementEntity saved = statementRepository.save(statement); + flushAndClear(); + + // Act + statementRepository.deleteById(saved.getId()); + flushAndClear(); + Optional retrieved = statementRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved).isEmpty(); } - @Nested - @DisplayName("Base Class Functionality") - class BaseClassTest { - - @Test - @DisplayName("Should inherit IdentifiedObject functionality") - void shouldInheritIdentifiedObjectFunctionality() { - // Arrange & Act - StatementEntity statement = TestDataBuilders.createValidStatement(); - - // Assert - assertThat(statement.getId()).isNotNull(); - assertThat(statement.getId()).isInstanceOf(UUID.class); - assertThat(statement.getRelatedLinks()).isNotNull(); - assertThat(statement.getRelatedLinks()).isEmpty(); - } - - @Test - @DisplayName("Should set timestamps on persist") - void shouldSetTimestampsOnPersist() { - // Arrange - StatementEntity statement = TestDataBuilders.createValidStatement(); - statement.setDescription("Statement for Timestamp Test"); - - // Act - StatementEntity saved = statementRepository.save(statement); - flushAndClear(); - - // Assert - assertThat(saved.getCreated()).isNotNull(); - assertThat(saved.getUpdated()).isNotNull(); - } + private StatementEntity createStatementWithCustomer(CustomerEntity customer) { + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Customer"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + statement.setCustomer(customer); + return statement; } -} \ No newline at end of file +} diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementMySQLIntegrationTest.java new file mode 100644 index 00000000..5bca7cb2 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementMySQLIntegrationTest.java @@ -0,0 +1,204 @@ +/* + * + * 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.repositories.integration; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.*; +import org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.*; +import org.greenbuttonalliance.espi.common.repositories.usage.UsageSummaryRepository; +import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.junit.jupiter.Container; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Statement entity integration tests using MySQL TestContainer. + * + * Tests Phase 19 ESPI 4.0 compliance with MySQL: + * - StatementRef as @ElementCollection (extends Object, not IdentifiedObject) + * - ID-based relationship queries (Customer, CustomerAccount, CustomerAgreement, UsageSummary) + * - Full CRUD operations with real MySQL database + */ +@DisplayName("Statement Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class StatementMySQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final org.testcontainers.containers.MySQLContainer mysql = mysqlContainer; + + static { + mysql.start(); + } + + @DynamicPropertySource + static void configureMySQLProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", mysql::getJdbcUrl); + registry.add("spring.datasource.username", mysql::getUsername); + registry.add("spring.datasource.password", mysql::getPassword); + registry.add("spring.datasource.driver-class-name", () -> "com.mysql.cj.jdbc.Driver"); + } + + @Autowired + private StatementRepository statementRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CustomerAccountRepository customerAccountRepository; + + @Autowired + private CustomerAgreementRepository customerAgreementRepository; + + @Autowired + private UsageSummaryRepository usageSummaryRepository; + + @Test + @DisplayName("Should persist statement with StatementRef @ElementCollection") + void shouldPersistStatementWithElementCollection() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("MySQL Statement Test"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + + List refs = new ArrayList<>(); + StatementRefEntity ref1 = new StatementRefEntity(); + ref1.setFileName("bill.pdf"); + ref1.setMediaType("application/pdf"); + ref1.setStatementURL("https://example.com/bills/001.pdf"); + refs.add(ref1); + + statement.setStatementRefs(refs); + + // Act + StatementEntity saved = statementRepository.save(statement); + entityManager.flush(); + entityManager.clear(); + Optional retrieved = statementRepository.findById(saved.getId()); + + // Assert - StatementRef stored in collection table without id column + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(stmt -> { + assertThat(stmt.getStatementRefs()).hasSize(1); + assertThat(stmt.getStatementRefs().get(0)) + .extracting("fileName", "mediaType", "statementURL") + .containsExactly("bill.pdf", "application/pdf", "https://example.com/bills/001.pdf"); + }); + } + + @Test + @DisplayName("Should support relationship queries with Customer") + void shouldSupportCustomerRelationship() { + // Arrange + CustomerEntity customer = new CustomerEntity(); + customer.setCustomerName("Test Customer"); + CustomerEntity savedCustomer = customerRepository.save(customer); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Customer"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + statement.setCustomer(savedCustomer); + statementRepository.save(statement); + + entityManager.flush(); + entityManager.clear(); + + // Act + List found = statementRepository.findByCustomerId(savedCustomer.getId()); + + // Assert + assertThat(found).hasSize(1) + .first() + .extracting(s -> s.getCustomer().getId()) + .isEqualTo(savedCustomer.getId()); + } + + @Test + @DisplayName("Should support all relationship queries") + void shouldSupportAllRelationshipQueries() { + // Arrange + CustomerAccountEntity account = new CustomerAccountEntity(); + account.setDescription("Test Account"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + + CustomerAgreementEntity agreement = new CustomerAgreementEntity(); + agreement.setDescription("Test Agreement"); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + + UsageSummaryEntity summary = new UsageSummaryEntity(); + summary.setDescription("Test Summary"); + UsageSummaryEntity savedSummary = usageSummaryRepository.save(summary); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement with All Relationships"); + statement.setCustomerAccount(savedAccount); + statement.setCustomerAgreement(savedAgreement); + statement.setUsageSummary(savedSummary); + statementRepository.save(statement); + + entityManager.flush(); + entityManager.clear(); + + // Act & Assert - All relationship queries work + assertThat(statementRepository.findByCustomerAccountId(savedAccount.getId())).hasSize(1); + assertThat(statementRepository.findByCustomerAgreementId(savedAgreement.getId())).hasSize(1); + assertThat(statementRepository.findByUsageSummaryId(savedSummary.getId())).hasSize(1); + } + + @Test + @DisplayName("Should handle cascading delete of @ElementCollection") + void shouldCascadeDeleteElementCollection() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement to Delete"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + + List refs = new ArrayList<>(); + StatementRefEntity ref = new StatementRefEntity(); + ref.setFileName("test.pdf"); + ref.setMediaType("application/pdf"); + ref.setStatementURL("https://example.com/test.pdf"); + refs.add(ref); + statement.setStatementRefs(refs); + + StatementEntity saved = statementRepository.save(statement); + entityManager.flush(); + entityManager.clear(); + + // Act - Delete statement should cascade to collection + statementRepository.deleteById(saved.getId()); + entityManager.flush(); + entityManager.clear(); + + // Assert + assertThat(statementRepository.findById(saved.getId())).isEmpty(); + } +} \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementPostgreSQLIntegrationTest.java new file mode 100644 index 00000000..ed96905d --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/StatementPostgreSQLIntegrationTest.java @@ -0,0 +1,204 @@ +/* + * + * 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.repositories.integration; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.*; +import org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.*; +import org.greenbuttonalliance.espi.common.repositories.usage.UsageSummaryRepository; +import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.junit.jupiter.Container; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Statement entity integration tests using PostgreSQL TestContainer. + * + * Tests Phase 19 ESPI 4.0 compliance: + * - StatementRef as @ElementCollection (extends Object, not IdentifiedObject) + * - ID-based relationship queries (Customer, CustomerAccount, CustomerAgreement, UsageSummary) + * - Full CRUD operations with real PostgreSQL database + */ +@DisplayName("Statement Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class StatementPostgreSQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final org.testcontainers.containers.PostgreSQLContainer postgres = postgresqlContainer; + + static { + postgres.start(); + } + + @DynamicPropertySource + static void configurePostgreSQLProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver"); + } + + @Autowired + private StatementRepository statementRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CustomerAccountRepository customerAccountRepository; + + @Autowired + private CustomerAgreementRepository customerAgreementRepository; + + @Autowired + private UsageSummaryRepository usageSummaryRepository; + + @Test + @DisplayName("Should persist statement with StatementRef @ElementCollection") + void shouldPersistStatementWithElementCollection() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("PostgreSQL Statement Test"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + + List refs = new ArrayList<>(); + StatementRefEntity ref1 = new StatementRefEntity(); + ref1.setFileName("bill.pdf"); + ref1.setMediaType("application/pdf"); + ref1.setStatementURL("https://example.com/bills/001.pdf"); + refs.add(ref1); + + statement.setStatementRefs(refs); + + // Act + StatementEntity saved = statementRepository.save(statement); + entityManager.flush(); + entityManager.clear(); + Optional retrieved = statementRepository.findById(saved.getId()); + + // Assert - StatementRef stored in collection table without id column + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(stmt -> { + assertThat(stmt.getStatementRefs()).hasSize(1); + assertThat(stmt.getStatementRefs().get(0)) + .extracting("fileName", "mediaType", "statementURL") + .containsExactly("bill.pdf", "application/pdf", "https://example.com/bills/001.pdf"); + }); + } + + @Test + @DisplayName("Should support relationship queries with Customer") + void shouldSupportCustomerRelationship() { + // Arrange + CustomerEntity customer = new CustomerEntity(); + customer.setCustomerName("Test Customer"); + CustomerEntity savedCustomer = customerRepository.save(customer); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement for Customer"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + statement.setCustomer(savedCustomer); + statementRepository.save(statement); + + entityManager.flush(); + entityManager.clear(); + + // Act + List found = statementRepository.findByCustomerId(savedCustomer.getId()); + + // Assert + assertThat(found).hasSize(1) + .first() + .extracting(s -> s.getCustomer().getId()) + .isEqualTo(savedCustomer.getId()); + } + + @Test + @DisplayName("Should support all relationship queries") + void shouldSupportAllRelationshipQueries() { + // Arrange + CustomerAccountEntity account = new CustomerAccountEntity(); + account.setDescription("Test Account"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + + CustomerAgreementEntity agreement = new CustomerAgreementEntity(); + agreement.setDescription("Test Agreement"); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + + UsageSummaryEntity summary = new UsageSummaryEntity(); + summary.setDescription("Test Summary"); + UsageSummaryEntity savedSummary = usageSummaryRepository.save(summary); + + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement with All Relationships"); + statement.setCustomerAccount(savedAccount); + statement.setCustomerAgreement(savedAgreement); + statement.setUsageSummary(savedSummary); + statementRepository.save(statement); + + entityManager.flush(); + entityManager.clear(); + + // Act & Assert - All relationship queries work + assertThat(statementRepository.findByCustomerAccountId(savedAccount.getId())).hasSize(1); + assertThat(statementRepository.findByCustomerAgreementId(savedAgreement.getId())).hasSize(1); + assertThat(statementRepository.findByUsageSummaryId(savedSummary.getId())).hasSize(1); + } + + @Test + @DisplayName("Should handle cascading delete of @ElementCollection") + void shouldCascadeDeleteElementCollection() { + // Arrange + StatementEntity statement = new StatementEntity(); + statement.setDescription("Statement to Delete"); + statement.setIssueDateTime(OffsetDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS)); + + List refs = new ArrayList<>(); + StatementRefEntity ref = new StatementRefEntity(); + ref.setFileName("test.pdf"); + ref.setMediaType("application/pdf"); + ref.setStatementURL("https://example.com/test.pdf"); + refs.add(ref); + statement.setStatementRefs(refs); + + StatementEntity saved = statementRepository.save(statement); + entityManager.flush(); + entityManager.clear(); + + // Act - Delete statement should cascade to collection + statementRepository.deleteById(saved.getId()); + entityManager.flush(); + entityManager.clear(); + + // Assert + assertThat(statementRepository.findById(saved.getId())).isEmpty(); + } +}