diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ProgramDateIdMapping.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ProgramDateIdMapping.java new file mode 100644 index 00000000..c3b65286 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ProgramDateIdMapping.java @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.domain.customer.common; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; + +import java.io.Serializable; + +/** + * Embeddable class for single customer energy efficiency program date mapping. + * + * Per customer.xsd lines 1223-1251, ProgramDateIdMapping extends Object (NOT IdentifiedObject). + * This is an embedded component, not a standalone entity. + */ +@Embeddable +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProgramDateIdMapping implements Serializable { + + /** + * Type of customer energy efficiency program date. + */ + @Enumerated(EnumType.STRING) + @Column(name = "program_date_type", length = 64) + private ProgramDateKind programDateType; + + /** + * Code value (may be alphanumeric). + */ + @Column(name = "code", length = 64) + private String code; + + /** + * Name associated with code. + */ + @Column(name = "name", length = 256) + private String name; + + /** + * Optional description of code. + */ + @Column(name = "note", length = 256) + private String note; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java index a31b735c..cad155aa 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java @@ -21,6 +21,7 @@ import lombok.*; import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; +import org.greenbuttonalliance.espi.common.domain.customer.common.ProgramDateIdMapping; import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.*; @@ -28,8 +29,10 @@ /** * Pure JPA/Hibernate entity for ProgramDateIdMappings without JAXB concerns. - * - * [extension] Collection of all customer Energy Efficiency programs + * + * [extension] Collection of all customer Energy Efficiency programs. + * Per customer.xsd lines 269-283, ProgramDateIdMappings extends IdentifiedObject + * and contains a single embedded ProgramDateIdMapping object. */ @Entity @Table(name = "program_date_id_mappings") @@ -46,10 +49,39 @@ public class ProgramDateIdMappingsEntity extends IdentifiedObject { /** - * [extension] Program date description - * TODO: Create ProgramDateIdMappingEntity and enable this relationship + * Single customer energy efficiency program date mapping. + * Per customer.xsd, this is the only field in ProgramDateIdMappings beyond IdentifiedObject fields. */ - // @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - // @JoinColumn(name = "program_date_id_mapping_id") - // private ProgramDateIdMappingEntity programDateIdMapping; + @Embedded + private ProgramDateIdMapping programDateIdMapping; + + @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; + ProgramDateIdMappingsEntity that = (ProgramDateIdMappingsEntity) 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() + ", " + + "description = " + getDescription() + ", " + + "created = " + getCreated() + ", " + + "updated = " + getUpdated() + ", " + + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + + "programDateIdMapping = " + programDateIdMapping + ", " + + "relatedLinks = " + getRelatedLinks() + ")"; + } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/ProgramDateKind.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/ProgramDateKind.java new file mode 100644 index 00000000..5556108c --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/ProgramDateKind.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.domain.customer.enums; + +/** + * Type of Demand Response program date based on ESPI 4.0 customer.xsd specification. + * + * Per customer.xsd lines 1997-2030. + * Note: XSD uses union type, allowing both enumerated values and custom String64 values. + * + * Ordinal mapping: + * 0 = CUST_DR_PROGRAM_ENROLLMENT_DATE + * 1 = CUST_DR_PROGRAM_DE_ENROLLMENT_DATE + * 2 = CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL + * 3 = CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL + */ +public enum ProgramDateKind { + /** + * Date customer enrolled in Demand Response program. + * Ordinal: 0 + */ + CUST_DR_PROGRAM_ENROLLMENT_DATE, + + /** + * Date customer terminated enrollment in Demand Response program. + * Ordinal: 1 + */ + CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + + /** + * Earliest date customer can terminate Demand Response enrollment, regardless of financial impact. + * Ordinal: 2 + */ + CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, + + /** + * Earliest date customer can terminate Demand Response enrollment, without financial impact. + * Ordinal: 3 + */ + CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingDto.java new file mode 100644 index 00000000..32519ae2 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingDto.java @@ -0,0 +1,85 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.dto.customer; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; + +import java.io.Serializable; + +/** + * ProgramDateIdMapping DTO for embedded complex type. + * Per ESPI 4.0 customer.xsd lines 1223-1251. + * + * Single customer energy efficiency program date mapping with 4 fields: + * - programDateType: Type of customer energy efficiency program date + * - code: Code value (may be alphanumeric) + * - name: Name associated with code + * - note: Optional description of code + * + * This is an embedded component (extends Object in XSD, not IdentifiedObject). + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ProgramDateIdMapping", namespace = "http://naesb.org/espi/customer", propOrder = { + "programDateType", "code", "name", "note" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProgramDateIdMappingDto implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Type of customer energy efficiency program date. + * Maps to customer.xsd programDateType element (lines 1224-1238). + */ + @XmlElement(name = "programDateType", namespace = "http://naesb.org/espi/customer") + private ProgramDateKind programDateType; + + /** + * Code value (may be alphanumeric). + * Maps to customer.xsd code element (lines 1239-1242). + */ + @XmlElement(name = "code", namespace = "http://naesb.org/espi/customer") + private String code; + + /** + * Name associated with code. + * Maps to customer.xsd name element (lines 1243-1246). + */ + @XmlElement(name = "name", namespace = "http://naesb.org/espi/customer") + private String name; + + /** + * Optional description of code. + * Maps to customer.xsd note element (lines 1247-1250). + */ + @XmlElement(name = "note", namespace = "http://naesb.org/espi/customer") + private String note; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java index 61b323ae..e95ba4fa 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java @@ -19,102 +19,46 @@ 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; +import java.io.Serializable; /** - * ProgramDateIdMappings DTO class for JAXB XML marshalling/unmarshalling. + * ProgramDateIdMappings DTO for ESPI 4.0 customer.xsd schema compliance. + * Per customer.xsd lines 269-283. + * + * ProgramDateIdMappings is an ESPI Resource that inherits from IdentifiedObject. + * + * [extension] Collection of all customer Energy Efficiency programs. + * Contains a single embedded ProgramDateIdMapping object with customer energy efficiency + * program date mapping information. * - * Represents mappings between program dates and identifiers. - * Supports Atom protocol XML wrapping. + * This DTO contains ONLY the 1 XSD element from customer.xsd: + * - programDateIdMapping: Single customer energy efficiency program date mapping + * + * Atom protocol fields (id, published, updated, links) are handled by CustomerAtomEntryDto wrapper. */ @XmlRootElement(name = "ProgramDateIdMappings", namespace = "http://naesb.org/espi/customer") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "ProgramDateIdMappings", namespace = "http://naesb.org/espi/customer", propOrder = { - "published", "updated", "selfLink", "upLink", "relatedLinks", - "description", "programId", "programDate", "mappingId", "mappingType", - "isActive", "customer" + "programDateIdMapping" }) @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class ProgramDateIdMappingsDto { - - @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; +public class ProgramDateIdMappingsDto implements Serializable { - @XmlElement(name = "programId") - private String programId; - - @XmlElement(name = "programDate") - private OffsetDateTime programDate; - - @XmlElement(name = "mappingId") - private String mappingId; - - @XmlElement(name = "mappingType") - private String mappingType; - - @XmlElement(name = "isActive") - private Boolean isActive; - - @XmlElement(name = "Customer") - private CustomerDto customer; - - /** - * Minimal constructor for basic mapping data. - */ - public ProgramDateIdMappingsDto(String uuid, String programId, String mappingId) { - this(null, uuid, null, null, null, null, null, null, - programId, null, mappingId, null, null, null); - } - - /** - * Gets the self href for this mapping. - * - * @return self href string - */ - public String getSelfHref() { - return selfLink != null ? selfLink.getHref() : null; - } + private static final long serialVersionUID = 1L; /** - * Gets the up href for this mapping. - * - * @return up href string + * Single customer energy efficiency program date mapping. + * Maps to customer.xsd ProgramDateIdMapping element (lines 270-282). */ - public String getUpHref() { - return upLink != null ? upLink.getHref() : null; - } + @XmlElement(name = "ProgramDateIdMapping", namespace = "http://naesb.org/espi/customer") + private ProgramDateIdMappingDto programDateIdMapping; } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingMapper.java new file mode 100644 index 00000000..b8f91691 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingMapper.java @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.common.ProgramDateIdMapping; +import org.greenbuttonalliance.espi.common.dto.customer.ProgramDateIdMappingDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for converting between ProgramDateIdMapping and ProgramDateIdMappingDto. + *

+ * Maps customer energy efficiency program date mapping fields per customer.xsd ProgramDateIdMapping type (lines 1223-1251). + * Includes programDateType, code, name, and note fields from ESPI 4.0 customer.xsd. + */ +@Mapper(componentModel = "spring", uses = {DateTimeMapper.class}) +public interface ProgramDateIdMappingMapper { + + /** + * Converts ProgramDateIdMapping entity to ProgramDateIdMappingDto. + * Maps the 4 fields defined in customer.xsd: programDateType, code, name, note. + * + * @param entity the program date ID mapping entity + * @return the program date ID mapping DTO + */ + @Mapping(target = "programDateType", source = "programDateType") + @Mapping(target = "code", source = "code") + @Mapping(target = "name", source = "name") + @Mapping(target = "note", source = "note") + ProgramDateIdMappingDto toDto(ProgramDateIdMapping entity); + + /** + * Converts ProgramDateIdMappingDto to ProgramDateIdMapping entity. + * Maps the 4 fields from customer.xsd. + * + * @param dto the program date ID mapping DTO + * @return the program date ID mapping entity + */ + @Mapping(target = "programDateType", source = "programDateType") + @Mapping(target = "code", source = "code") + @Mapping(target = "name", source = "name") + @Mapping(target = "note", source = "note") + ProgramDateIdMapping toEntity(ProgramDateIdMappingDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingsMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingsMapper.java new file mode 100644 index 00000000..4f0d8731 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingsMapper.java @@ -0,0 +1,69 @@ +/* + * + * 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.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.dto.customer.ProgramDateIdMappingsDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +/** + * MapStruct mapper for converting between ProgramDateIdMappingsEntity and ProgramDateIdMappingsDto. + * + * Maps only ProgramDateIdMappings fields. IdentifiedObject fields are NOT part of the customer.xsd + * definition and are handled by AtomFeedDto/AtomEntryDto. + * + * Handles the conversion between the JPA entity used for persistence and the DTO + * used for JAXB XML marshalling in the Green Button API. + */ +@Mapper(componentModel = "spring", uses = { + DateTimeMapper.class, + ProgramDateIdMappingMapper.class +}) +public interface ProgramDateIdMappingsMapper { + + /** + * Converts a ProgramDateIdMappingsEntity to a ProgramDateIdMappingsDto. + * Maps the embedded program date ID mapping information. + * + * @param entity the program date ID mappings entity + * @return the program date ID mappings DTO + */ + ProgramDateIdMappingsDto toDto(ProgramDateIdMappingsEntity entity); + + /** + * Converts a ProgramDateIdMappingsDto to a ProgramDateIdMappingsEntity. + * Maps program date ID mapping information from DTO to entity. + * + * @param dto the program date ID mappings DTO + * @return the program date ID mappings entity + */ + ProgramDateIdMappingsEntity toEntity(ProgramDateIdMappingsDto dto); + + /** + * Updates an existing ProgramDateIdMappingsEntity with data from a ProgramDateIdMappingsDto. + * + * @param dto the program date ID mappings DTO with updated data + * @param entity the existing program date ID mappings entity to update + */ + void updateEntityFromDto(ProgramDateIdMappingsDto dto, @MappingTarget ProgramDateIdMappingsEntity entity); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepository.java new file mode 100644 index 00000000..dcc144e9 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepository.java @@ -0,0 +1,38 @@ +/* + * + * 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.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +/** + * Spring Data JPA repository for ProgramDateIdMappingsEntity entities. + *

+ * Manages customer energy efficiency program date mappings. + *

+ * Only provides basic CRUD operations via JpaRepository. No custom queries are needed + * as the only allowed queries are by ID. + */ +@Repository +public interface ProgramDateIdMappingsRepository extends JpaRepository { +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ProgramDateIdMappingsService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ProgramDateIdMappingsService.java new file mode 100644 index 00000000..ef4f077a --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/ProgramDateIdMappingsService.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.service.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; + +import java.util.Optional; +import java.util.UUID; + +/** + * Service interface for ProgramDateIdMappings management. + * + * Handles business logic for customer energy efficiency program date mappings. + * Only ID-based queries are supported. + */ +public interface ProgramDateIdMappingsService { + + /** + * Find program date ID mappings by ID. + * + * @param id the program date ID mappings UUID + * @return optional containing the program date ID mappings if found + */ + Optional findById(UUID id); + + /** + * Save program date ID mappings. + * + * @param programDateIdMappings the program date ID mappings to save + * @return the saved program date ID mappings + */ + ProgramDateIdMappingsEntity save(ProgramDateIdMappingsEntity programDateIdMappings); + + /** + * Delete program date ID mappings by ID. + * + * @param id the program date ID mappings UUID to delete + */ + void deleteById(UUID id); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ProgramDateIdMappingsServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ProgramDateIdMappingsServiceImpl.java new file mode 100644 index 00000000..e8f55524 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ProgramDateIdMappingsServiceImpl.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.service.customer.impl; + +import lombok.RequiredArgsConstructor; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.ProgramDateIdMappingsRepository; +import org.greenbuttonalliance.espi.common.service.customer.ProgramDateIdMappingsService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +/** + * Service implementation for ProgramDateIdMappings management. + *

+ * Provides business logic for customer energy efficiency program date mappings. + * Only ID-based queries are supported. + */ +@Service +@Transactional +@RequiredArgsConstructor +public class ProgramDateIdMappingsServiceImpl implements ProgramDateIdMappingsService { + + private final ProgramDateIdMappingsRepository programDateIdMappingsRepository; + + @Override + @Transactional(readOnly = true) + public Optional findById(UUID id) { + return programDateIdMappingsRepository.findById(id); + } + + @Override + public ProgramDateIdMappingsEntity save(ProgramDateIdMappingsEntity programDateIdMappings) { + return programDateIdMappingsRepository.save(programDateIdMappings); + } + + @Override + public void deleteById(UUID id) { + programDateIdMappingsRepository.deleteById(id); + } +} 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 27dbac3e..b96ed008 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 @@ -678,6 +678,8 @@ CREATE INDEX idx_phone_number_created ON phone_numbers (created); CREATE INDEX idx_phone_number_updated ON phone_numbers (updated); -- Program Date ID Mappings Table +-- Per customer.xsd lines 269-283, ProgramDateIdMappings extends IdentifiedObject +-- and contains embedded ProgramDateIdMapping (lines 1223-1251) CREATE TABLE program_date_id_mappings ( id CHAR(36) PRIMARY KEY , @@ -692,16 +694,13 @@ CREATE TABLE program_date_id_mappings self_link_href VARCHAR(1024), self_link_type VARCHAR(255), - -- Program date ID mapping specific fields - program_date BIGINT, - program_id VARCHAR(100) + -- ProgramDateIdMapping embedded fields (customer.xsd lines 1223-1251) + program_date_type VARCHAR(64), + code VARCHAR(64), + name VARCHAR(256), + note VARCHAR(256) ); -CREATE INDEX idx_program_date_id_mapping_program_date ON program_date_id_mappings (program_date); -CREATE INDEX idx_program_date_id_mapping_program_id ON program_date_id_mappings (program_id); -CREATE INDEX idx_program_date_id_mapping_created ON program_date_id_mappings (created); -CREATE INDEX idx_program_date_id_mapping_updated ON program_date_id_mappings (updated); - -- Related Links Table for Program Date ID Mappings CREATE TABLE program_date_id_mapping_related_links ( diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepositoryTest.java new file mode 100644 index 00000000..21091a96 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepositoryTest.java @@ -0,0 +1,341 @@ +/* + * + * 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.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.common.ProgramDateIdMapping; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; +import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest; +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 java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test suite for ProgramDateIdMappingsRepository. + * + * Tests all CRUD operations and validation constraints for ProgramDateIdMappings entities. + * Per ESPI 4.0 API specification, only ID-based queries are supported. + */ +@DisplayName("ProgramDateIdMappings Repository Tests") +class ProgramDateIdMappingsRepositoryTest extends BaseRepositoryTest { + + @Autowired + private ProgramDateIdMappingsRepository programDateIdMappingsRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve program date ID mappings successfully") + void shouldSaveAndRetrieveProgramDateIdMappingsSuccessfully() { + // Arrange + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setDescription("Test DR Program Enrollment"); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(saved.getId()).isNotNull(); + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(mapping -> assertThat(mapping) + .extracting( + ProgramDateIdMappingsEntity::getDescription, + m -> m.getProgramDateIdMapping().getProgramDateType(), + m -> m.getProgramDateIdMapping().getCode() + ) + .containsExactly("Test DR Program Enrollment", + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "DR-001")); + } + + @Test + @DisplayName("Should find all program date ID mappings") + void shouldFindAllProgramDateIdMappings() { + // Arrange + ProgramDateIdMappingsEntity mapping1 = createValidProgramDateIdMappings(); + mapping1.setDescription("Mapping 1"); + ProgramDateIdMappingsEntity mapping2 = createValidProgramDateIdMappings(); + mapping2.setDescription("Mapping 2"); + ProgramDateIdMappingsEntity mapping3 = createValidProgramDateIdMappings(); + mapping3.setDescription("Mapping 3"); + + programDateIdMappingsRepository.saveAll(List.of(mapping1, mapping2, mapping3)); + flushAndClear(); + + // Act + List allMappings = programDateIdMappingsRepository.findAll(); + + // Assert + assertThat(allMappings) + .hasSizeGreaterThanOrEqualTo(3) + .extracting(ProgramDateIdMappingsEntity::getDescription) + .contains("Mapping 1", "Mapping 2", "Mapping 3"); + } + + @Test + @DisplayName("Should delete program date ID mappings successfully") + void shouldDeleteProgramDateIdMappingsSuccessfully() { + // Arrange + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setDescription("To Be Deleted"); + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + UUID id = saved.getId(); + flushAndClear(); + + // Act + programDateIdMappingsRepository.deleteById(id); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(id); + + // Assert + assertThat(retrieved).isEmpty(); + } + + @Test + @DisplayName("Should check if program date ID mappings exists") + void shouldCheckIfProgramDateIdMappingsExists() { + // Arrange + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setDescription("Exists Check"); + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + + // Act & Assert + assertThat(programDateIdMappingsRepository.existsById(saved.getId())).isTrue(); + assertThat(programDateIdMappingsRepository.existsById(UUID.randomUUID())).isFalse(); + } + + @Test + @DisplayName("Should count program date ID mappings") + void shouldCountProgramDateIdMappings() { + // Arrange + long initialCount = programDateIdMappingsRepository.count(); + programDateIdMappingsRepository.saveAll(List.of( + createValidProgramDateIdMappings(), + createValidProgramDateIdMappings(), + createValidProgramDateIdMappings() + )); + flushAndClear(); + + // Act & Assert + assertThat(programDateIdMappingsRepository.count()).isEqualTo(initialCount + 3); + } + } + + @Nested + @DisplayName("Embedded Object Persistence") + class EmbeddedObjectPersistenceTest { + + @Test + @DisplayName("Should persist all ProgramDateIdMapping fields correctly") + void shouldPersistAllProgramDateIdMappingFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + "CODE-123", + "Program De-enrollment", + "Optional note about de-enrollment" + ); + + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setProgramDateIdMapping(mapping); + entity.setDescription("Full Field Test"); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()) + .isNotNull() + .extracting( + ProgramDateIdMapping::getProgramDateType, + ProgramDateIdMapping::getCode, + ProgramDateIdMapping::getName, + ProgramDateIdMapping::getNote + ) + .containsExactly( + ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + "CODE-123", + "Program De-enrollment", + "Optional note about de-enrollment" + )); + } + + @Test + @DisplayName("Should persist all ProgramDateKind enum values") + void shouldPersistAllProgramDateKindEnumValues() { + // Test all 4 enum values to ensure they persist correctly + ProgramDateKind[] allKinds = ProgramDateKind.values(); + + for (ProgramDateKind kind : allKinds) { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + kind, + "CODE-" + kind.ordinal(), + "Test " + kind.name(), + "Note for " + kind.name() + ); + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .as("Should persist enum value: %s", kind) + .isPresent() + .hasValueSatisfying(e -> + assertThat(e.getProgramDateIdMapping().getProgramDateType()) + .isEqualTo(kind)); + } + } + + @Test + @DisplayName("Should handle null embedded object") + void shouldHandleNullEmbeddedObject() { + // Arrange + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setDescription("No embedded mapping"); + entity.setProgramDateIdMapping(null); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()).isNull()); + } + + @Test + @DisplayName("Should handle embedded object with null fields") + void shouldHandleEmbeddedObjectWithNullFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping(); + mapping.setProgramDateType(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE); + // Leave code, name, note as null + + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()) + .isNotNull() + .satisfies(m -> { + assertThat(m.getProgramDateType()) + .isEqualTo(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE); + assertThat(m.getCode()).isNull(); + assertThat(m.getName()).isNull(); + assertThat(m.getNote()).isNull(); + })); + } + } + + @Nested + @DisplayName("IdentifiedObject Fields") + class IdentifiedObjectFieldsTest { + + @Test + @DisplayName("Should persist IdentifiedObject metadata fields") + void shouldPersistIdentifiedObjectMetadataFields() { + // Arrange + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + entity.setDescription("Metadata Test"); + + // Act + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + Optional retrieved = programDateIdMappingsRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e) + .extracting( + ProgramDateIdMappingsEntity::getId, + ProgramDateIdMappingsEntity::getDescription, + ProgramDateIdMappingsEntity::getCreated, + ProgramDateIdMappingsEntity::getUpdated + ) + .doesNotContainNull()); + } + + @Test + @DisplayName("Should update timestamps on modification") + void shouldUpdateTimestampsOnModification() throws InterruptedException { + // Arrange + ProgramDateIdMappingsEntity entity = createValidProgramDateIdMappings(); + ProgramDateIdMappingsEntity saved = programDateIdMappingsRepository.save(entity); + flushAndClear(); + + // Act + Thread.sleep(10); // Ensure timestamp difference + saved.setDescription("Updated Description"); + ProgramDateIdMappingsEntity updated = programDateIdMappingsRepository.save(saved); + flushAndClear(); + + // Assert + assertThat(updated.getUpdated()).isAfter(updated.getCreated()); + } + } + + /** + * Helper method to create a valid ProgramDateIdMappingsEntity for testing. + */ + private ProgramDateIdMappingsEntity createValidProgramDateIdMappings() { + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "DR-001", + "Demand Response Enrollment", + "Test program enrollment" + ); + + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + return entity; + } +} diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsMySQLIntegrationTest.java new file mode 100644 index 00000000..080f9a03 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsMySQLIntegrationTest.java @@ -0,0 +1,321 @@ +/* + * + * 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.common.ProgramDateIdMapping; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; +import org.greenbuttonalliance.espi.common.repositories.customer.ProgramDateIdMappingsRepository; +import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest; +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 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.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ProgramDateIdMappings entity integration tests using MySQL TestContainer. + * + * Tests full CRUD operations and embedded object persistence with a real MySQL database. + */ +@DisplayName("ProgramDateIdMappings Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class ProgramDateIdMappingsMySQLIntegrationTest 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 ProgramDateIdMappingsRepository repository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve program date ID mappings with all fields") + void shouldSaveAndRetrieveProgramDateIdMappingsWithAllFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "MYSQL-CODE-001", + "MySQL DR Program Enrollment", + "Test note for MySQL enrollment" + ); + + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setDescription("MySQL Integration Test"); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(result -> assertThat(result) + .extracting( + ProgramDateIdMappingsEntity::getDescription, + e -> e.getProgramDateIdMapping().getProgramDateType(), + e -> e.getProgramDateIdMapping().getCode(), + e -> e.getProgramDateIdMapping().getName(), + e -> e.getProgramDateIdMapping().getNote() + ) + .containsExactly( + "MySQL Integration Test", + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "MYSQL-CODE-001", + "MySQL DR Program Enrollment", + "Test note for MySQL enrollment" + )); + } + + @Test + @DisplayName("Should update program date ID mappings fields") + void shouldUpdateProgramDateIdMappingsFields() { + // Arrange + ProgramDateIdMapping originalMapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "ORIGINAL-CODE", + "Original Name", + "Original Note" + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(originalMapping); + entity.setDescription("Original Description"); + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + + // Act + ProgramDateIdMapping updatedMapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + "UPDATED-CODE", + "Updated Name", + "Updated Note" + ); + savedEntity.setProgramDateIdMapping(updatedMapping); + savedEntity.setDescription("Updated Description"); + ProgramDateIdMappingsEntity updated = repository.save(savedEntity); + flushAndClear(); + + Optional retrieved = repository.findById(updated.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(result -> assertThat(result) + .extracting( + ProgramDateIdMappingsEntity::getDescription, + e -> e.getProgramDateIdMapping().getProgramDateType(), + e -> e.getProgramDateIdMapping().getCode() + ) + .containsExactly( + "Updated Description", + ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + "UPDATED-CODE" + )); + } + + @Test + @DisplayName("Should delete program date ID mappings") + void shouldDeleteProgramDateIdMappings() { + // Arrange + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "DELETE-ME", + "To Be Deleted", + null + )); + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + + // Act + repository.deleteById(savedEntity.getId()); + flushAndClear(); + + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List entities = List.of( + createEntity(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, "BULK-1"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, "BULK-2"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, "BULK-3"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL, "BULK-4") + ); + + // Act + List savedEntities = repository.saveAll(entities); + flushAndClear(); + + // Assert + assertThat(savedEntities).hasSize(4).allMatch(e -> e.getId() != null); + assertThat(repository.count()).isGreaterThanOrEqualTo(4); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List entities = List.of( + createEntity(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, "DELETE-1"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, "DELETE-2") + ); + + List savedEntities = repository.saveAll(entities); + long initialCount = repository.count(); + flushAndClear(); + + // Act + repository.deleteAll(savedEntities); + flushAndClear(); + + // Assert + assertThat(repository.count()).isEqualTo(initialCount - 2); + } + } + + @Nested + @DisplayName("Embedded Object Persistence") + class EmbeddedObjectPersistenceTest { + + @Test + @DisplayName("Should persist all ProgramDateKind enum values") + void shouldPersistAllProgramDateKindEnumValues() { + for (ProgramDateKind kind : ProgramDateKind.values()) { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + kind, + "MYSQL-" + kind.ordinal(), + "MySQL Test " + kind.name(), + "Note for " + kind.name() + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .as("Should persist enum value: %s", kind) + .isPresent() + .hasValueSatisfying(e -> + assertThat(e.getProgramDateIdMapping().getProgramDateType()) + .isEqualTo(kind)); + } + } + + @Test + @DisplayName("Should handle null embedded object") + void shouldHandleNullEmbeddedObject() { + // Arrange + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setDescription("Null Embedded Object Test"); + entity.setProgramDateIdMapping(null); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()).isNull()); + } + + @Test + @DisplayName("Should handle embedded object with partial null fields") + void shouldHandleEmbeddedObjectWithPartialNullFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping(); + mapping.setProgramDateType(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE); + mapping.setCode("PARTIAL-CODE"); + // Leave name and note as null + + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()) + .isNotNull() + .satisfies(m -> { + assertThat(m.getCode()).isEqualTo("PARTIAL-CODE"); + assertThat(m.getName()).isNull(); + assertThat(m.getNote()).isNull(); + })); + } + } + + private ProgramDateIdMappingsEntity createEntity(ProgramDateKind kind, String code) { + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + kind, + code, + "Test " + kind.name(), + "Note for " + code + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + return entity; + } +} diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsPostgreSQLIntegrationTest.java new file mode 100644 index 00000000..1955405a --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ProgramDateIdMappingsPostgreSQLIntegrationTest.java @@ -0,0 +1,321 @@ +/* + * + * 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.common.ProgramDateIdMapping; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; +import org.greenbuttonalliance.espi.common.repositories.customer.ProgramDateIdMappingsRepository; +import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest; +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 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.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ProgramDateIdMappings entity integration tests using PostgreSQL TestContainer. + * + * Tests full CRUD operations and embedded object persistence with a real PostgreSQL database. + */ +@DisplayName("ProgramDateIdMappings Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class ProgramDateIdMappingsPostgreSQLIntegrationTest 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 ProgramDateIdMappingsRepository repository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve program date ID mappings with all fields") + void shouldSaveAndRetrieveProgramDateIdMappingsWithAllFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL, + "POSTGRES-CODE-001", + "PostgreSQL DR Program Term Date", + "Test note for PostgreSQL termination without financial impact" + ); + + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setDescription("PostgreSQL Integration Test"); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(result -> assertThat(result) + .extracting( + ProgramDateIdMappingsEntity::getDescription, + e -> e.getProgramDateIdMapping().getProgramDateType(), + e -> e.getProgramDateIdMapping().getCode(), + e -> e.getProgramDateIdMapping().getName(), + e -> e.getProgramDateIdMapping().getNote() + ) + .containsExactly( + "PostgreSQL Integration Test", + ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL, + "POSTGRES-CODE-001", + "PostgreSQL DR Program Term Date", + "Test note for PostgreSQL termination without financial impact" + )); + } + + @Test + @DisplayName("Should update program date ID mappings fields") + void shouldUpdateProgramDateIdMappingsFields() { + // Arrange + ProgramDateIdMapping originalMapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, + "ORIG-POSTGRES-CODE", + "Original PostgreSQL Name", + "Original PostgreSQL Note" + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(originalMapping); + entity.setDescription("Original PostgreSQL Description"); + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + + // Act + ProgramDateIdMapping updatedMapping = new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, + "UPD-POSTGRES-CODE", + "Updated PostgreSQL Name", + "Updated PostgreSQL Note" + ); + savedEntity.setProgramDateIdMapping(updatedMapping); + savedEntity.setDescription("Updated PostgreSQL Description"); + ProgramDateIdMappingsEntity updated = repository.save(savedEntity); + flushAndClear(); + + Optional retrieved = repository.findById(updated.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(result -> assertThat(result) + .extracting( + ProgramDateIdMappingsEntity::getDescription, + e -> e.getProgramDateIdMapping().getProgramDateType(), + e -> e.getProgramDateIdMapping().getCode() + ) + .containsExactly( + "Updated PostgreSQL Description", + ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, + "UPD-POSTGRES-CODE" + )); + } + + @Test + @DisplayName("Should delete program date ID mappings") + void shouldDeleteProgramDateIdMappings() { + // Arrange + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(new ProgramDateIdMapping( + ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + "DELETE-POSTGRES", + "To Be Deleted PostgreSQL", + null + )); + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + + // Act + repository.deleteById(savedEntity.getId()); + flushAndClear(); + + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List entities = List.of( + createEntity(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, "PG-BULK-1"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, "PG-BULK-2"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, "PG-BULK-3"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL, "PG-BULK-4") + ); + + // Act + List savedEntities = repository.saveAll(entities); + flushAndClear(); + + // Assert + assertThat(savedEntities).hasSize(4).allMatch(e -> e.getId() != null); + assertThat(repository.count()).isGreaterThanOrEqualTo(4); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List entities = List.of( + createEntity(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE, "PG-DELETE-1"), + createEntity(ProgramDateKind.CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, "PG-DELETE-2") + ); + + List savedEntities = repository.saveAll(entities); + long initialCount = repository.count(); + flushAndClear(); + + // Act + repository.deleteAll(savedEntities); + flushAndClear(); + + // Assert + assertThat(repository.count()).isEqualTo(initialCount - 2); + } + } + + @Nested + @DisplayName("Embedded Object Persistence") + class EmbeddedObjectPersistenceTest { + + @Test + @DisplayName("Should persist all ProgramDateKind enum values") + void shouldPersistAllProgramDateKindEnumValues() { + for (ProgramDateKind kind : ProgramDateKind.values()) { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + kind, + "PG-" + kind.ordinal(), + "PostgreSQL Test " + kind.name(), + "PostgreSQL note for " + kind.name() + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .as("Should persist enum value: %s", kind) + .isPresent() + .hasValueSatisfying(e -> + assertThat(e.getProgramDateIdMapping().getProgramDateType()) + .isEqualTo(kind)); + } + } + + @Test + @DisplayName("Should handle null embedded object") + void shouldHandleNullEmbeddedObject() { + // Arrange + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setDescription("PostgreSQL Null Embedded Object Test"); + entity.setProgramDateIdMapping(null); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()).isNull()); + } + + @Test + @DisplayName("Should handle embedded object with partial null fields") + void shouldHandleEmbeddedObjectWithPartialNullFields() { + // Arrange + ProgramDateIdMapping mapping = new ProgramDateIdMapping(); + mapping.setProgramDateType(ProgramDateKind.CUST_DR_PROGRAM_ENROLLMENT_DATE); + mapping.setCode("PG-PARTIAL-CODE"); + // Leave name and note as null + + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + + // Act + ProgramDateIdMappingsEntity savedEntity = repository.save(entity); + flushAndClear(); + Optional retrieved = repository.findById(savedEntity.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(e -> assertThat(e.getProgramDateIdMapping()) + .isNotNull() + .satisfies(m -> { + assertThat(m.getCode()).isEqualTo("PG-PARTIAL-CODE"); + assertThat(m.getName()).isNull(); + assertThat(m.getNote()).isNull(); + })); + } + } + + private ProgramDateIdMappingsEntity createEntity(ProgramDateKind kind, String code) { + ProgramDateIdMapping mapping = new ProgramDateIdMapping( + kind, + code, + "PostgreSQL Test " + kind.name(), + "PostgreSQL note for " + code + ); + ProgramDateIdMappingsEntity entity = new ProgramDateIdMappingsEntity(); + entity.setProgramDateIdMapping(mapping); + return entity; + } +}