diff --git a/openespi-common/.claude/commands/commit.md b/openespi-common/.claude/commands/commit.md new file mode 100644 index 00000000..26ab9a3a --- /dev/null +++ b/openespi-common/.claude/commands/commit.md @@ -0,0 +1,29 @@ +--- +description: Generate and create commit from staged changes +allowed-tools: Bash(git add:*), Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git commit:*) +model: haiku +--- + +## Context +- Current branch: !`git branch --show-current` +- Commits on this branch: !`git log main..HEAD --oneline` +- Full diff from main: !`git diff main...HEAD --stat` +## Task +Generate a pull request description that includes: +1. **Summary** - One paragraph explaining what this PR does +2. **Changes** - Bullet list of key changes based on commits +3. **Testing** - How to verify these changes work + Use the commit messages to understand intent. Keep the description concise but complete. + After generating, ask if I want to create the PR with `gh pr create`. + +## Context +- Current git status: !`git status` +- Staged changes: !`git diff --cached` +- Unstaged changes: !`git diff` +- Recent commits for style: !`git log --oneline -10` +## Task +Based on the staged changes, generate a conventional commit message. +Format: `type(scope): description` +Types: feat, fix, docs, style, refactor, test, chore +Match the commit style used in this repository's recent history. +After generating the message, ask if I want to commit with it. \ No newline at end of file diff --git a/openespi-common/.claude/commands/fix-pipeline.md b/openespi-common/.claude/commands/fix-pipeline.md new file mode 100644 index 00000000..6db83d0b --- /dev/null +++ b/openespi-common/.claude/commands/fix-pipeline.md @@ -0,0 +1,19 @@ +--- +description: Analyze failed CI pipeline and suggest fixes +allowed-tools: Bash(gh:*), Read, Write, Edit, Grep +model: sonnet +--- + +## Context +- Recent workflow runs: !`gh run list --limit 5` +## Task +Analyze the most recent failed CI run and fix the issue. +**Critical: You MUST read the actual error logs before proposing any fix.** +Steps: +1. Get the failed run ID from the list above +2. Run `gh run view --log-failed` to see actual errors +3. Analyze the root cause - not just the symptom +4. Search the codebase for relevant files +5. Implement a fix +6. Explain what failed and why your fix addresses it + Do not guess at solutions. The logs contain the answer. \ No newline at end of file diff --git a/openespi-common/.claude/commands/pr.md b/openespi-common/.claude/commands/pr.md new file mode 100644 index 00000000..5973c0ce --- /dev/null +++ b/openespi-common/.claude/commands/pr.md @@ -0,0 +1,5 @@ +--- +description: Generate PR description from branch commits +allowed-tools: Bash(git:*), Bash(gh:*) +model: haiku +--- \ No newline at end of file diff --git a/openespi-common/.claude/commands/release.md b/openespi-common/.claude/commands/release.md new file mode 100644 index 00000000..071232d2 --- /dev/null +++ b/openespi-common/.claude/commands/release.md @@ -0,0 +1,19 @@ +--- +description: Prepare a new release with version bump and changelog +allowed-tools: Bash(git:*), Bash(gh:*), Bash(npm:*), Read, Write, Edit +model: haiku +--- + +## Context +- Current version (package.json): !`cat package.json | grep '"version"' | head -1` +- Commits since last tag: !`git log $(git describe --tags --abbrev=0)..HEAD --oneline` +- Existing tags: !`git tag --sort=-v:refname | head -5` +## Task +Prepare a release. Ask me what version bump type: major, minor, or patch. +Then: +1. Update version in package.json +2. Generate changelog entry from commits since last tag +3. Group changes by type (Features, Fixes, Other) +4. Stage the changes +5. Ask if I want to create the git tag + Do not push or publish - just prepare locally for my review. \ No newline at end of file diff --git a/openespi-common/.claude/commands/security-scan.md b/openespi-common/.claude/commands/security-scan.md new file mode 100644 index 00000000..571b5e06 --- /dev/null +++ b/openespi-common/.claude/commands/security-scan.md @@ -0,0 +1,23 @@ +--- +description: Security scan of staged changes before commit +allowed-tools: Bash(git diff:*), Read, Grep +model: sonnet +--- +## Context +- Staged changes: !`git diff --cached` +- Changed files: !`git diff --cached --name-only` +## Task +Review the staged changes for security issues. +Check for: +- Hardcoded secrets, API keys, or credentials +- SQL injection vulnerabilities +- XSS attack vectors +- Insecure dependencies being added +- Authentication/authorization bypasses +- Sensitive data exposure + For each issue found: +1. File and line number +2. What the vulnerability is +3. How to fix it + If no issues found, confirm the changes are safe to commit. + Be thorough but avoid false positives on obvious non-issues. \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/LineItemEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/LineItemEntity.java index 5a141fa2..b4ac145e 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/LineItemEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/LineItemEntity.java @@ -25,9 +25,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.JdbcTypeCode; +import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval; +import org.greenbuttonalliance.espi.common.domain.common.SummaryMeasurement; import org.hibernate.proxy.HibernateProxy; -import org.hibernate.type.SqlTypes; import java.math.BigDecimal; import java.math.RoundingMode; @@ -35,7 +35,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Objects; -import java.util.UUID; /** * Pure JPA/Hibernate entity for LineItem without JAXB concerns. @@ -61,12 +60,12 @@ public class LineItemEntity { /** * Primary key identifier. + * LineItem extends Object (not IdentifiedObject) per ESPI 4.0 XSD (espi.xsd:1449). */ @Id - @GeneratedValue(strategy = GenerationType.UUID) - @JdbcTypeCode(SqlTypes.CHAR) - @Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false) - private UUID id; + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(updatable = false, nullable = false) + private Long id; /** * Amount for this line item in currency minor units (e.g., cents). @@ -100,6 +99,47 @@ public class LineItemEntity { @Size(max = 256, message = "Note cannot exceed 256 characters") private String note; + /** + * Relevant measurement for line item (optional). + * Per ESPI 4.0 XSD (espi.xsd:1471), extension field. + */ + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "powerOfTenMultiplier", column = @Column(name = "measurement_multiplier")), + @AttributeOverride(name = "timeStamp", column = @Column(name = "measurement_timestamp")), + @AttributeOverride(name = "uom", column = @Column(name = "measurement_uom")), + @AttributeOverride(name = "value", column = @Column(name = "measurement_value")), + @AttributeOverride(name = "readingTypeRef", column = @Column(name = "measurement_reading_type_ref", length = 512)) + }) + private SummaryMeasurement measurement; + + /** + * Classification of line item (required). + * Per ESPI 4.0 XSD (espi.xsd:1476), extension field. + * ItemKind enumeration values (e.g., 1=Energy Generation Fee, 2=Energy Delivery Fee, etc.) + */ + @Column(name = "item_kind", nullable = false) + @NotNull(message = "Item kind cannot be null") + private Integer itemKind; + + /** + * Per unit cost (optional). + * Per ESPI 4.0 XSD (espi.xsd:1481), extension field. + */ + @Column(name = "unit_cost") + private Long unitCost; + + /** + * Time period covered by the line item (optional). + * Per ESPI 4.0 XSD (espi.xsd:1486), extension field to support pricing changes mid-billing period. + */ + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "start", column = @Column(name = "item_period_start")), + @AttributeOverride(name = "duration", column = @Column(name = "item_period_duration")) + }) + private DateTimeInterval itemPeriod; + // ElectricPowerUsageSummary relationship removed - deprecated resource /** @@ -111,12 +151,14 @@ public class LineItemEntity { private UsageSummaryEntity usageSummary; /** - * Constructor with basic line item information. - * + * Constructor with basic line item information (legacy, pre-ESPI 4.0 compliance). + * @deprecated Use constructor with itemKind parameter for ESPI 4.0 compliance. + * * @param amount the amount in currency minor units * @param dateTime the timestamp * @param note the descriptive note */ + @Deprecated public LineItemEntity(Long amount, Long dateTime, String note) { this.amount = amount; this.dateTime = dateTime; @@ -124,13 +166,30 @@ public LineItemEntity(Long amount, Long dateTime, String note) { } /** - * Constructor with full line item information. - * + * Constructor with basic line item information and required itemKind. + * + * @param amount the amount in currency minor units + * @param dateTime the timestamp + * @param note the descriptive note + * @param itemKind the classification of the line item (required) + */ + public LineItemEntity(Long amount, Long dateTime, String note, Integer itemKind) { + this.amount = amount; + this.dateTime = dateTime; + this.note = note; + this.itemKind = itemKind; + } + + /** + * Constructor with full line item information (legacy, pre-ESPI 4.0 compliance). + * @deprecated Use constructor with itemKind parameter for ESPI 4.0 compliance. + * * @param amount the amount in currency minor units * @param rounding the rounding adjustment * @param dateTime the timestamp * @param note the descriptive note */ + @Deprecated public LineItemEntity(Long amount, Long rounding, Long dateTime, String note) { this.amount = amount; this.rounding = rounding; @@ -138,6 +197,48 @@ public LineItemEntity(Long amount, Long rounding, Long dateTime, String note) { this.note = note; } + /** + * Constructor with full line item information including required itemKind. + * + * @param amount the amount in currency minor units + * @param rounding the rounding adjustment + * @param dateTime the timestamp + * @param note the descriptive note + * @param itemKind the classification of the line item (required) + */ + public LineItemEntity(Long amount, Long rounding, Long dateTime, String note, Integer itemKind) { + this.amount = amount; + this.rounding = rounding; + this.dateTime = dateTime; + this.note = note; + this.itemKind = itemKind; + } + + /** + * Constructor with complete line item information including all optional fields. + * + * @param amount the amount in currency minor units + * @param rounding the rounding adjustment + * @param dateTime the timestamp + * @param note the descriptive note + * @param measurement relevant measurement for line item + * @param itemKind the classification of the line item (required) + * @param unitCost per unit cost + * @param itemPeriod time period covered by the line item + */ + public LineItemEntity(Long amount, Long rounding, Long dateTime, String note, + SummaryMeasurement measurement, Integer itemKind, + Long unitCost, DateTimeInterval itemPeriod) { + this.amount = amount; + this.rounding = rounding; + this.dateTime = dateTime; + this.note = note; + this.measurement = measurement; + this.itemKind = itemKind; + this.unitCost = unitCost; + this.itemPeriod = itemPeriod; + } + // ElectricPowerUsageSummary setter removed - deprecated resource /** diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java new file mode 100644 index 00000000..0829de43 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java @@ -0,0 +1,233 @@ +/* + * + * 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.usage; + +import jakarta.xml.bind.annotation.*; +import org.greenbuttonalliance.espi.common.dto.SummaryMeasurementDto; + +/** + * LineItem DTO record for JAXB XML marshalling/unmarshalling. + * + * Represents a line item of detail for additional cost. + * Contains billing line item details including amount, rounding, timestamp, + * descriptive note, measurement, classification, unit cost, and period. + * + * Per ESPI 4.0 XSD (espi.xsd:1444-1494), LineItem extends Object (not IdentifiedObject), + * so it has no UUID, selfLink, upLink, or other Atom metadata. + * + * Part of the NAESB ESPI UsageSummary structure for detailed cost breakdowns. + */ +@XmlRootElement(name = "LineItem", namespace = "http://naesb.org/espi") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(name = "LineItem", namespace = "http://naesb.org/espi", propOrder = { + "amount", "rounding", "dateTime", "note", "measurement", "itemKind", "unitCost", "itemPeriod" +}) +public record LineItemDto( + Long amount, + Long rounding, + Long dateTime, + String note, + SummaryMeasurementDto measurement, + Integer itemKind, + Long unitCost, + DateTimeIntervalDto itemPeriod +) { + + /** + * Cost of line item in currency minor units (e.g., cents). + */ + @XmlElement(name = "amount") + public Long getAmount() { + return amount; + } + + /** + * Rounding adjustment applied to the line item amount. + */ + @XmlElement(name = "rounding") + public Long getRounding() { + return rounding; + } + + /** + * Significant date/time for this line item. + * Stored as epoch seconds (TimeType in ESPI). + */ + @XmlElement(name = "dateTime") + public Long getDateTime() { + return dateTime; + } + + /** + * Comment or description of the line item. + */ + @XmlElement(name = "note") + public String getNote() { + return note; + } + + /** + * Relevant measurement for the line item (extension field). + * Contains measurement value, unit, multiplier, and reading type reference. + */ + @XmlElement(name = "measurement") + public SummaryMeasurementDto getMeasurement() { + return measurement; + } + + /** + * Classification of the line item (extension field). + * ItemKind enumeration values (e.g., 1=Energy Generation Fee, 2=Energy Delivery Fee, etc.). + */ + @XmlElement(name = "itemKind") + public Integer getItemKind() { + return itemKind; + } + + /** + * Per unit cost (extension field). + * Cost per unit of measurement. + */ + @XmlElement(name = "unitCost") + public Long getUnitCost() { + return unitCost; + } + + /** + * Time period covered by this line item (extension field). + * Supports pricing changes in the middle of a billing period. + */ + @XmlElement(name = "itemPeriod") + public DateTimeIntervalDto getItemPeriod() { + return itemPeriod; + } + + /** + * Default constructor for JAXB. + */ + public LineItemDto() { + this(null, null, null, null, null, null, null, null); + } + + /** + * Constructor with basic line item information. + * + * @param amount the amount in currency minor units + * @param dateTime the significant date/time + * @param note the descriptive note + * @param itemKind the classification of the line item + */ + public LineItemDto(Long amount, Long dateTime, String note, Integer itemKind) { + this(amount, null, dateTime, note, null, itemKind, null, null); + } + + /** + * Checks if this line item has rounding applied. + * + * @return true if rounding is non-zero + */ + public boolean hasRounding() { + return rounding != null && rounding != 0; + } + + /** + * Gets the total amount including rounding. + * + * @return total amount, or just amount if no rounding + */ + public Long getTotalAmount() { + if (rounding == null) { + return amount; + } + return amount != null ? amount + rounding : rounding; + } + + /** + * Checks if this line item represents a charge (positive amount). + * + * @return true if amount is positive + */ + public boolean isCharge() { + return amount != null && amount > 0; + } + + /** + * Checks if this line item represents a credit (negative amount). + * + * @return true if amount is negative + */ + public boolean isCredit() { + return amount != null && amount < 0; + } + + /** + * Checks if this line item has a measurement value. + * + * @return true if measurement is present + */ + public boolean hasMeasurement() { + return measurement != null; + } + + /** + * Checks if this line item has a time period specified. + * + * @return true if itemPeriod is present + */ + public boolean hasItemPeriod() { + return itemPeriod != null; + } + + /** + * Validates the line item data. + * + * @return true if valid (note and itemKind are required) + */ + public boolean isValid() { + return note != null && !note.trim().isEmpty() && itemKind != null; + } + + /** + * Creates a basic LineItem with required fields. + * + * @param amount the amount + * @param dateTime the date/time + * @param note the note + * @param itemKind the item kind + * @return new LineItemDto + */ + public static LineItemDto create(Long amount, Long dateTime, String note, Integer itemKind) { + return new LineItemDto(amount, null, dateTime, note, null, itemKind, null, null); + } + + /** + * Creates a LineItem with amount, rounding, and basic fields. + * + * @param amount the amount + * @param rounding the rounding + * @param dateTime the date/time + * @param note the note + * @param itemKind the item kind + * @return new LineItemDto + */ + public static LineItemDto createWithRounding(Long amount, Long rounding, Long dateTime, String note, Integer itemKind) { + return new LineItemDto(amount, rounding, dateTime, note, null, itemKind, null, null); + } +} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/SummaryMeasurementMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/SummaryMeasurementMapper.java new file mode 100644 index 00000000..94140d63 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/SummaryMeasurementMapper.java @@ -0,0 +1,50 @@ +/* + * + * 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; + +import org.greenbuttonalliance.espi.common.domain.common.SummaryMeasurement; +import org.greenbuttonalliance.espi.common.dto.SummaryMeasurementDto; +import org.mapstruct.Mapper; + +/** + * MapStruct mapper for converting between SummaryMeasurement and SummaryMeasurementDto. + * + * Handles the conversion between the embedded value object used in entities and the DTO + * used for JAXB XML marshalling in the Green Button API. + */ +@Mapper(componentModel = "spring") +public interface SummaryMeasurementMapper { + + /** + * Converts a SummaryMeasurement to a SummaryMeasurementDto. + * + * @param measurement the summary measurement value object + * @return the summary measurement DTO + */ + SummaryMeasurementDto toDto(SummaryMeasurement measurement); + + /** + * Converts a SummaryMeasurementDto to a SummaryMeasurement. + * + * @param dto the summary measurement DTO + * @return the summary measurement value object + */ + SummaryMeasurement toEntity(SummaryMeasurementDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/LineItemMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/LineItemMapper.java new file mode 100644 index 00000000..c2f89d56 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/LineItemMapper.java @@ -0,0 +1,54 @@ +/* + * + * 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.usage; + +import org.greenbuttonalliance.espi.common.domain.usage.LineItemEntity; +import org.greenbuttonalliance.espi.common.dto.usage.LineItemDto; +import org.greenbuttonalliance.espi.common.mapper.SummaryMeasurementMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for converting between LineItemEntity and LineItemDto. + *

+ * LineItem extends Object (not IdentifiedObject) in ESPI 4.0, so it does not + * have Atom links or timestamps - only business data fields. + */ +@Mapper(componentModel = "spring", uses = {SummaryMeasurementMapper.class, DateTimeIntervalMapper.class}) +public interface LineItemMapper { + + /** + * Converts a LineItemEntity to a LineItemDto. + * + * @param entity the line item entity + * @return the line item DTO + */ + LineItemDto toDto(LineItemEntity entity); + + /** + * Converts a LineItemDto to a LineItemEntity. + * + * @param dto the line item DTO + * @return the line item entity + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "usageSummary", ignore = true) + LineItemEntity toEntity(LineItemDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepository.java index 74d0f37b..a28c4c00 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepository.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepository.java @@ -28,40 +28,22 @@ import java.util.List; import java.util.UUID; +/** + * Spring Data JPA repository for LineItemEntity. + *

+ * LineItem extends Object (not IdentifiedObject) in ESPI 4.0 XSD, + * so it uses Long ID (not UUID). + *

+ * Following established pattern from Phases 7-9, keeping only queries using indexed columns: + * - findAllIds() uses primary key + * - findAllByUsageSummaryId() uses foreign key index (usage_summary_id) + */ @Repository -public interface LineItemRepository extends JpaRepository { - // JpaRepository provides: save(), findAll(), findById(), deleteById(), etc. - // Note: merge() functionality is handled by save() in Spring Data JPA - - // All 12 original NamedQueries from LineItemEntity: - - @Query("SELECT li FROM LineItemEntity li WHERE li.usageSummary.id = :electricPowerUsageSummaryId ORDER BY li.dateTime") - List findByElectricPowerUsageSummaryId(@Param("electricPowerUsageSummaryId") UUID electricPowerUsageSummaryId); - - @Query("SELECT li FROM LineItemEntity li WHERE li.usageSummary.id = :usageSummaryId ORDER BY li.dateTime") - List findByUsageSummaryId(@Param("usageSummaryId") UUID usageSummaryId); - - @Query("SELECT li FROM LineItemEntity li WHERE li.dateTime >= :startTime AND li.dateTime <= :endTime ORDER BY li.dateTime") - List findByDateTimeRange(@Param("startTime") Long startTime, @Param("endTime") Long endTime); - - @Query("SELECT li FROM LineItemEntity li WHERE li.amount >= :minAmount AND li.amount <= :maxAmount ORDER BY li.amount DESC") - List findByAmountRange(@Param("minAmount") Long minAmount, @Param("maxAmount") Long maxAmount); - - @Query("SELECT li FROM LineItemEntity li WHERE LOWER(li.note) LIKE LOWER(CONCAT('%', :searchText, '%')) ORDER BY li.dateTime") - List findByNoteContaining(@Param("searchText") String searchText); +public interface LineItemRepository extends JpaRepository { @Query("SELECT li.id FROM LineItemEntity li") - List findAllIds(); - - @Query("SELECT SUM(li.amount) FROM LineItemEntity li WHERE li.usageSummary.id = :electricPowerUsageSummaryId") - Long sumAmountsByElectricPowerUsageSummary(@Param("electricPowerUsageSummaryId") UUID electricPowerUsageSummaryId); + List findAllIds(); - @Query("SELECT SUM(li.amount) FROM LineItemEntity li WHERE li.usageSummary.id = :usageSummaryId") - Long sumAmountsByUsageSummary(@Param("usageSummaryId") UUID usageSummaryId); - - @Query("SELECT COUNT(li) FROM LineItemEntity li WHERE li.usageSummary.id = :electricPowerUsageSummaryId") - Long countByElectricPowerUsageSummary(@Param("electricPowerUsageSummaryId") UUID electricPowerUsageSummaryId); - - @Query("SELECT COUNT(li) FROM LineItemEntity li WHERE li.usageSummary.id = :usageSummaryId") - Long countByUsageSummary(@Param("usageSummaryId") UUID usageSummaryId); + @Query("SELECT li FROM LineItemEntity li WHERE li.usageSummary.id = :usageSummaryId ORDER BY li.dateTime") + List findAllByUsageSummaryId(@Param("usageSummaryId") UUID usageSummaryId); } \ 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 e8d0868e..da1bad5e 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 @@ -18,15 +18,17 @@ * - interval_block_related_links (FK dependency) * - interval_readings (extends Object, requires vendor-specific auto-increment, depends on interval_blocks) * - reading_qualities (extends Object, requires vendor-specific auto-increment, depends on interval_readings) + * - usage_summaries + usage_summary_related_links (moved to maintain dependency order with line_items) * * Reason: IntervalReading and ReadingQuality both extend Object (not IdentifiedObject) per ESPI 4.0 XSD * (espi.xsd:1016 and espi.xsd:1062), requiring Long ID with vendor-specific auto-increment syntax * (BIGINT AUTO_INCREMENT for MySQL/H2, BIGSERIAL for PostgreSQL). To keep the dependency chain together * (meter_readings → interval_blocks → interval_readings → reading_qualities), all were moved to V2 vendor files. * + * LineItem also extends Object (espi.xsd:1449) and requires vendor-specific auto-increment. Since LineItem + * has a foreign key to usage_summaries, usage_summaries was moved to V2 to maintain proper dependency order. + * * Tables in this migration: - * - usage_summaries (depends on usage_points from V2) - * - usage_summary_related_links (FK dependency) * - subscription_usage_points (join table) * - aggregated_node_refs (depends on pnode_refs from V2) * - customer schema tables @@ -61,130 +63,14 @@ -- db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql -- db/vendor/h2/V2__H2_Specific_Tables.sql --- Usage Summary Table -CREATE TABLE usage_summaries -( - 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), - - -- Usage summary specific fields - bill_last_period BIGINT, - bill_to_date BIGINT, - cost_additional_last_period BIGINT, - currency VARCHAR(3), - quality_of_reading VARCHAR(50), - status_timestamp BIGINT, - - -- Embedded DateTimeInterval: billingPeriod - billing_period_start BIGINT, - billing_period_duration BIGINT, - - -- Embedded DateTimeInterval: ratchetDemandPeriod - ratchet_demand_period_start BIGINT, - ratchet_demand_period_duration BIGINT, - - -- Embedded SummaryMeasurement: overallConsumptionLastPeriod - overall_consumption_last_period_multiplier VARCHAR(255), - overall_consumption_last_period_timestamp BIGINT, - overall_consumption_last_period_uom VARCHAR(50), - overall_consumption_last_period_value BIGINT, - overall_consumption_last_period_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: currentBillingPeriodOverAllConsumption - current_billing_period_overall_consumption_multiplier VARCHAR(255), - current_billing_period_overall_consumption_timestamp BIGINT, - current_billing_period_overall_consumption_uom VARCHAR(50), - current_billing_period_overall_consumption_value BIGINT, - current_billing_period_overall_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: currentDayLastYearNetConsumption - current_day_last_year_net_consumption_multiplier VARCHAR(255), - current_day_last_year_net_consumption_timestamp BIGINT, - current_day_last_year_net_consumption_uom VARCHAR(50), - current_day_last_year_net_consumption_value BIGINT, - current_day_last_year_net_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: currentDayNetConsumption - current_day_net_consumption_multiplier VARCHAR(255), - current_day_net_consumption_timestamp BIGINT, - current_day_net_consumption_uom VARCHAR(50), - current_day_net_consumption_value BIGINT, - current_day_net_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: currentDayOverallConsumption - current_day_overall_consumption_multiplier VARCHAR(255), - current_day_overall_consumption_timestamp BIGINT, - current_day_overall_consumption_uom VARCHAR(50), - current_day_overall_consumption_value BIGINT, - current_day_overall_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: peakDemand - peak_demand_multiplier VARCHAR(255), - peak_demand_timestamp BIGINT, - peak_demand_uom VARCHAR(50), - peak_demand_value BIGINT, - peak_demand_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: previousDayLastYearOverallConsumption - previous_day_last_year_overall_consumption_multiplier VARCHAR(255), - previous_day_last_year_overall_consumption_timestamp BIGINT, - previous_day_last_year_overall_consumption_uom VARCHAR(50), - previous_day_last_year_overall_consumption_value BIGINT, - previous_day_last_year_overall_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: previousDayNetConsumption - previous_day_net_consumption_multiplier VARCHAR(255), - previous_day_net_consumption_timestamp BIGINT, - previous_day_net_consumption_uom VARCHAR(50), - previous_day_net_consumption_value BIGINT, - previous_day_net_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: previousDayOverallConsumption - previous_day_overall_consumption_multiplier VARCHAR(255), - previous_day_overall_consumption_timestamp BIGINT, - previous_day_overall_consumption_uom VARCHAR(50), - previous_day_overall_consumption_value BIGINT, - previous_day_overall_consumption_reading_type_ref VARCHAR(512), - - -- Embedded SummaryMeasurement: ratchetDemand - ratchet_demand_multiplier VARCHAR(255), - ratchet_demand_timestamp BIGINT, - ratchet_demand_uom VARCHAR(50), - ratchet_demand_value BIGINT, - ratchet_demand_reading_type_ref VARCHAR(512), - - -- Foreign key relationships - usage_point_id CHAR(36), - - FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE -); - --- Indexes for usage_summaries table -CREATE INDEX idx_usage_summary_usage_point_id ON usage_summaries (usage_point_id); -CREATE INDEX idx_usage_summary_billing_period_start ON usage_summaries (billing_period_start); -CREATE INDEX idx_usage_summary_created ON usage_summaries (created); -CREATE INDEX idx_usage_summary_updated ON usage_summaries (updated); - - --- Related Links Table for Usage Summaries -CREATE TABLE usage_summary_related_links -( - usage_summary_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE -); - --- Indexes for usage_summary_related_links table -CREATE INDEX idx_usage_summary_related_links ON usage_summary_related_links (usage_summary_id); +-- Usage Summary Table - Moved to vendor-specific V2 migration files +-- UsageSummary table moved to V2 migration files because LineItem (which extends Object) +-- requires vendor-specific auto-increment syntax and has a foreign key to usage_summaries. +-- To maintain proper dependency order, usage_summaries must be created before line_items. +-- Table creation moved to V2 vendor files. +-- See: db/vendor/mysql/V2__MySQL_Specific_Tables.sql +-- db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql +-- db/vendor/h2/V2__H2_Specific_Tables.sql -- Join Table for Subscription-UsagePoint Many-to-Many Relationship CREATE TABLE subscription_usage_points @@ -547,38 +433,12 @@ CREATE TABLE end_device_related_links CREATE INDEX idx_end_device_related_links ON end_device_related_links (end_device_id); --- Line Item Table -CREATE TABLE line_items -( - 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), - - -- Line item specific fields - amount BIGINT NOT NULL, - rounding BIGINT, - date_time BIGINT NOT NULL, - note VARCHAR(256) NOT NULL, - - -- Foreign key relationships - usage_summary_id CHAR(36), - - FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE -); - -CREATE INDEX idx_line_item_usage_summary ON line_items (usage_summary_id); -CREATE INDEX idx_line_item_date_time ON line_items (date_time); -CREATE INDEX idx_line_item_amount ON line_items (amount); -CREATE INDEX idx_line_item_created ON line_items (created); -CREATE INDEX idx_line_item_updated ON line_items (updated); +-- Line Item Table - Moved to vendor-specific V2 migration files +-- LineItem extends Object (not IdentifiedObject) per ESPI 4.0 XSD (espi.xsd:1449) +-- Table creation moved to V2 vendor files due to auto-increment syntax differences +-- See: db/vendor/mysql/V2__MySQL_Specific_Tables.sql +-- db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql +-- db/vendor/h2/V2__H2_Specific_Tables.sql -- Meter Entity Table (Joined inheritance from EndDevice) CREATE TABLE meters diff --git a/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql index 712f8004..bd334b0b 100644 --- a/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql @@ -9,20 +9,23 @@ * column type requirements. * * Tables included: + * - service_delivery_points (extends Object - vendor-specific AUTO_INCREMENT) * - time_configurations (with BINARY columns for dst_end_rule, dst_start_rule) * - usage_points (with BINARY column for role_flags) * - time_configuration_related_links (FK dependency) * - usage_point_related_links (FK dependency) + * - pnode_refs (extends Object - vendor-specific AUTO_INCREMENT, FK to usage_points) + * - aggregated_node_refs (extends Object - vendor-specific AUTO_INCREMENT, FK to usage_points) + * - aggregated_node_ref_pnode_refs (join table) * - meter_readings (FK dependency on usage_points) * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) - * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) - * - usage_summaries (FK dependency on usage_points) + * - interval_readings (extends Object - vendor-specific AUTO_INCREMENT, FK to interval_blocks) + * - reading_qualities (extends Object - vendor-specific AUTO_INCREMENT, FK to interval_readings) + * - usage_summaries (FK dependency on usage_points, must precede line_items) * - usage_summary_related_links (FK dependency) - * - subscription_usage_points (join table) - * - customer schema tables (FK dependency on time_configurations) + * - line_items (extends Object - vendor-specific AUTO_INCREMENT, FK to usage_summaries) * * Total tables in this migration: 25+ * Compatible with: H2 Database @@ -343,3 +346,166 @@ CREATE TABLE reading_qualities -- Create indexes for reading_qualities table CREATE INDEX idx_reading_quality_interval_reading_id ON reading_qualities (interval_reading_id); CREATE INDEX idx_reading_quality_quality ON reading_qualities (quality); + +-- Usage Summary Table (IdentifiedObject-based entity with UUID) +-- Must be created before line_items which references it +CREATE TABLE usage_summaries +( + id CHAR(36) PRIMARY KEY , + description VARCHAR(255), + created TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + updated TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + published TIMESTAMP(6), + 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), + + -- Usage summary specific fields + bill_last_period BIGINT, + bill_to_date BIGINT, + cost_additional_last_period BIGINT, + currency VARCHAR(3), + quality_of_reading VARCHAR(50), + status_timestamp BIGINT, + + -- Embedded DateTimeInterval: billingPeriod + billing_period_start BIGINT, + billing_period_duration BIGINT, + + -- Embedded DateTimeInterval: ratchetDemandPeriod + ratchet_demand_period_start BIGINT, + ratchet_demand_period_duration BIGINT, + + -- Embedded SummaryMeasurement: overallConsumptionLastPeriod + overall_consumption_last_period_multiplier VARCHAR(255), + overall_consumption_last_period_timestamp BIGINT, + overall_consumption_last_period_uom VARCHAR(50), + overall_consumption_last_period_value BIGINT, + overall_consumption_last_period_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentBillingPeriodOverAllConsumption + current_billing_period_overall_consumption_multiplier VARCHAR(255), + current_billing_period_overall_consumption_timestamp BIGINT, + current_billing_period_overall_consumption_uom VARCHAR(50), + current_billing_period_overall_consumption_value BIGINT, + current_billing_period_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayLastYearNetConsumption + current_day_last_year_net_consumption_multiplier VARCHAR(255), + current_day_last_year_net_consumption_timestamp BIGINT, + current_day_last_year_net_consumption_uom VARCHAR(50), + current_day_last_year_net_consumption_value BIGINT, + current_day_last_year_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayNetConsumption + current_day_net_consumption_multiplier VARCHAR(255), + current_day_net_consumption_timestamp BIGINT, + current_day_net_consumption_uom VARCHAR(50), + current_day_net_consumption_value BIGINT, + current_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayOverallConsumption + current_day_overall_consumption_multiplier VARCHAR(255), + current_day_overall_consumption_timestamp BIGINT, + current_day_overall_consumption_uom VARCHAR(50), + current_day_overall_consumption_value BIGINT, + current_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: peakDemand + peak_demand_multiplier VARCHAR(255), + peak_demand_timestamp BIGINT, + peak_demand_uom VARCHAR(50), + peak_demand_value BIGINT, + peak_demand_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayLastYearOverallConsumption + previous_day_last_year_overall_consumption_multiplier VARCHAR(255), + previous_day_last_year_overall_consumption_timestamp BIGINT, + previous_day_last_year_overall_consumption_uom VARCHAR(50), + previous_day_last_year_overall_consumption_value BIGINT, + previous_day_last_year_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayNetConsumption + previous_day_net_consumption_multiplier VARCHAR(255), + previous_day_net_consumption_timestamp BIGINT, + previous_day_net_consumption_uom VARCHAR(50), + previous_day_net_consumption_value BIGINT, + previous_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayOverallConsumption + previous_day_overall_consumption_multiplier VARCHAR(255), + previous_day_overall_consumption_timestamp BIGINT, + previous_day_overall_consumption_uom VARCHAR(50), + previous_day_overall_consumption_value BIGINT, + previous_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: ratchetDemand + ratchet_demand_multiplier VARCHAR(255), + ratchet_demand_timestamp BIGINT, + ratchet_demand_uom VARCHAR(50), + ratchet_demand_value BIGINT, + ratchet_demand_reading_type_ref VARCHAR(512), + + -- Foreign key relationships + usage_point_id CHAR(36), + + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE +); + +-- Create indexes for usage_summaries table +CREATE INDEX idx_usage_summary_usage_point_id ON usage_summaries (usage_point_id); +CREATE INDEX idx_usage_summary_billing_period_start ON usage_summaries (billing_period_start); +CREATE INDEX idx_usage_summary_created ON usage_summaries (created); +CREATE INDEX idx_usage_summary_updated ON usage_summaries (updated); + +-- Related Links Table for Usage Summaries +CREATE TABLE usage_summary_related_links +( + usage_summary_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE +); + +-- Create index for usage_summary_related_links table +CREATE INDEX idx_usage_summary_related_links ON usage_summary_related_links (usage_summary_id); + +-- Line Item Table (Object-based entity, no IdentifiedObject) +-- LineItem extends Object per ESPI 4.0 XSD (espi.xsd:1449) +-- XSD sequence: amount → rounding → dateTime → note → measurement → itemKind → unitCost → itemPeriod +CREATE TABLE line_items +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + + -- ESPI 4.0 fields in XSD sequence order + amount BIGINT, + rounding BIGINT, + date_time BIGINT, + note VARCHAR(256) NOT NULL, + + -- Embedded SummaryMeasurement: measurement + measurement_multiplier VARCHAR(255), + measurement_timestamp BIGINT, + measurement_uom VARCHAR(50), + measurement_value BIGINT, + measurement_reading_type_ref VARCHAR(512), + + item_kind INTEGER NOT NULL, + unit_cost BIGINT, + + -- Embedded DateTimeInterval: itemPeriod + item_period_start BIGINT, + item_period_duration BIGINT, + + -- Foreign key relationship (parent: UsageSummary) + usage_summary_id CHAR(36), + + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE +); + +-- Create indexes for line_items table +CREATE INDEX idx_line_item_usage_summary ON line_items (usage_summary_id); +CREATE INDEX idx_line_item_date_time ON line_items (date_time); +CREATE INDEX idx_line_item_amount ON line_items (amount); diff --git a/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql index 45e2a69c..f65dbf1d 100644 --- a/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql @@ -9,20 +9,23 @@ * column type requirements. * * Tables included: + * - service_delivery_points (extends Object - vendor-specific auto-increment) * - time_configurations (with BLOB columns for dst_end_rule, dst_start_rule) * - usage_points (with BLOB column for role_flags) * - time_configuration_related_links (FK dependency) * - usage_point_related_links (FK dependency) + * - pnode_refs (extends Object - vendor-specific auto-increment, FK to usage_points) + * - aggregated_node_refs (extends Object - vendor-specific auto-increment, FK to usage_points) + * - aggregated_node_ref_pnode_refs (join table) * - meter_readings (FK dependency on usage_points) * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) - * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) - * - usage_summaries (FK dependency on usage_points) + * - interval_readings (extends Object - vendor-specific auto-increment, FK to interval_blocks) + * - reading_qualities (extends Object - vendor-specific auto-increment, FK to interval_readings) + * - usage_summaries (FK dependency on usage_points, must precede line_items) * - usage_summary_related_links (FK dependency) - * - subscription_usage_points (join table) - * - customer schema tables (FK dependency on time_configurations) + * - line_items (extends Object - vendor-specific auto-increment, FK to usage_summaries) * * Total tables in this migration: 25+ * Compatible with: MySQL 8.0+ @@ -346,3 +349,162 @@ CREATE TABLE reading_qualities INDEX idx_reading_quality_interval_reading_id (interval_reading_id), INDEX idx_reading_quality_quality (quality) ); + +-- Usage Summary Table (IdentifiedObject-based entity with UUID) +-- Must be created before line_items which references it +CREATE TABLE usage_summaries +( + id CHAR(36) PRIMARY KEY , + description VARCHAR(255), + created DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + updated DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + published DATETIME(6), + 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), + + -- Usage summary specific fields + bill_last_period BIGINT, + bill_to_date BIGINT, + cost_additional_last_period BIGINT, + currency VARCHAR(3), + quality_of_reading VARCHAR(50), + status_timestamp BIGINT, + + -- Embedded DateTimeInterval: billingPeriod + billing_period_start BIGINT, + billing_period_duration BIGINT, + + -- Embedded DateTimeInterval: ratchetDemandPeriod + ratchet_demand_period_start BIGINT, + ratchet_demand_period_duration BIGINT, + + -- Embedded SummaryMeasurement: overallConsumptionLastPeriod + overall_consumption_last_period_multiplier VARCHAR(255), + overall_consumption_last_period_timestamp BIGINT, + overall_consumption_last_period_uom VARCHAR(50), + overall_consumption_last_period_value BIGINT, + overall_consumption_last_period_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentBillingPeriodOverAllConsumption + current_billing_period_overall_consumption_multiplier VARCHAR(255), + current_billing_period_overall_consumption_timestamp BIGINT, + current_billing_period_overall_consumption_uom VARCHAR(50), + current_billing_period_overall_consumption_value BIGINT, + current_billing_period_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayLastYearNetConsumption + current_day_last_year_net_consumption_multiplier VARCHAR(255), + current_day_last_year_net_consumption_timestamp BIGINT, + current_day_last_year_net_consumption_uom VARCHAR(50), + current_day_last_year_net_consumption_value BIGINT, + current_day_last_year_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayNetConsumption + current_day_net_consumption_multiplier VARCHAR(255), + current_day_net_consumption_timestamp BIGINT, + current_day_net_consumption_uom VARCHAR(50), + current_day_net_consumption_value BIGINT, + current_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayOverallConsumption + current_day_overall_consumption_multiplier VARCHAR(255), + current_day_overall_consumption_timestamp BIGINT, + current_day_overall_consumption_uom VARCHAR(50), + current_day_overall_consumption_value BIGINT, + current_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: peakDemand + peak_demand_multiplier VARCHAR(255), + peak_demand_timestamp BIGINT, + peak_demand_uom VARCHAR(50), + peak_demand_value BIGINT, + peak_demand_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayLastYearOverallConsumption + previous_day_last_year_overall_consumption_multiplier VARCHAR(255), + previous_day_last_year_overall_consumption_timestamp BIGINT, + previous_day_last_year_overall_consumption_uom VARCHAR(50), + previous_day_last_year_overall_consumption_value BIGINT, + previous_day_last_year_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayNetConsumption + previous_day_net_consumption_multiplier VARCHAR(255), + previous_day_net_consumption_timestamp BIGINT, + previous_day_net_consumption_uom VARCHAR(50), + previous_day_net_consumption_value BIGINT, + previous_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayOverallConsumption + previous_day_overall_consumption_multiplier VARCHAR(255), + previous_day_overall_consumption_timestamp BIGINT, + previous_day_overall_consumption_uom VARCHAR(50), + previous_day_overall_consumption_value BIGINT, + previous_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: ratchetDemand + ratchet_demand_multiplier VARCHAR(255), + ratchet_demand_timestamp BIGINT, + ratchet_demand_uom VARCHAR(50), + ratchet_demand_value BIGINT, + ratchet_demand_reading_type_ref VARCHAR(512), + + -- Foreign key relationships + usage_point_id CHAR(36), + + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE, + + INDEX idx_usage_summary_usage_point_id (usage_point_id), + INDEX idx_usage_summary_billing_period_start (billing_period_start), + INDEX idx_usage_summary_created (created), + INDEX idx_usage_summary_updated (updated) +); + +-- Related Links Table for Usage Summaries +CREATE TABLE usage_summary_related_links +( + usage_summary_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE, + INDEX idx_usage_summary_related_links (usage_summary_id) +); + +-- Line Item Table (Object-based entity, no IdentifiedObject) +-- LineItem extends Object per ESPI 4.0 XSD (espi.xsd:1449) +-- XSD sequence: amount → rounding → dateTime → note → measurement → itemKind → unitCost → itemPeriod +CREATE TABLE line_items +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + + -- ESPI 4.0 fields in XSD sequence order + amount BIGINT, + rounding BIGINT, + date_time BIGINT, + note VARCHAR(256) NOT NULL, + + -- Embedded SummaryMeasurement: measurement + measurement_multiplier VARCHAR(255), + measurement_timestamp BIGINT, + measurement_uom VARCHAR(50), + measurement_value BIGINT, + measurement_reading_type_ref VARCHAR(512), + + item_kind INTEGER NOT NULL, + unit_cost BIGINT, + + -- Embedded DateTimeInterval: itemPeriod + item_period_start BIGINT, + item_period_duration BIGINT, + + -- Foreign key relationship (parent: UsageSummary) + usage_summary_id CHAR(36), + + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE, + + INDEX idx_line_item_usage_summary (usage_summary_id), + INDEX idx_line_item_date_time (date_time), + INDEX idx_line_item_amount (amount) +); diff --git a/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql index 2a8aeab9..aaf64fe2 100644 --- a/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql @@ -9,20 +9,23 @@ * column type requirements. * * Tables included: + * - service_delivery_points (extends Object - vendor-specific BIGSERIAL) * - time_configurations (with BYTEA columns for dst_end_rule, dst_start_rule) * - usage_points (with BYTEA column for role_flags) * - time_configuration_related_links (FK dependency) * - usage_point_related_links (FK dependency) + * - pnode_refs (extends Object - vendor-specific BIGSERIAL, FK to usage_points) + * - aggregated_node_refs (extends Object - vendor-specific BIGSERIAL, FK to usage_points) + * - aggregated_node_ref_pnode_refs (join table) * - meter_readings (FK dependency on usage_points) * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) - * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) - * - usage_summaries (FK dependency on usage_points) + * - interval_readings (extends Object - vendor-specific BIGSERIAL, FK to interval_blocks) + * - reading_qualities (extends Object - vendor-specific BIGSERIAL, FK to interval_readings) + * - usage_summaries (FK dependency on usage_points, must precede line_items) * - usage_summary_related_links (FK dependency) - * - subscription_usage_points (join table) - * - customer schema tables (FK dependency on time_configurations) + * - line_items (extends Object - vendor-specific BIGSERIAL, FK to usage_summaries) * * Total tables in this migration: 25+ * Compatible with: PostgreSQL 12+ @@ -334,3 +337,163 @@ CREATE TABLE reading_qualities CREATE INDEX idx_reading_quality_interval_reading_id ON reading_qualities (interval_reading_id); CREATE INDEX idx_reading_quality_quality ON reading_qualities (quality); + +-- Usage Summary Table (IdentifiedObject-based entity with UUID) +-- Must be created before line_items which references it +CREATE TABLE usage_summaries +( + id CHAR(36) PRIMARY KEY , + description VARCHAR(255), + created TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + published TIMESTAMP(6), + 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), + + -- Usage summary specific fields + bill_last_period BIGINT, + bill_to_date BIGINT, + cost_additional_last_period BIGINT, + currency VARCHAR(3), + quality_of_reading VARCHAR(50), + status_timestamp BIGINT, + + -- Embedded DateTimeInterval: billingPeriod + billing_period_start BIGINT, + billing_period_duration BIGINT, + + -- Embedded DateTimeInterval: ratchetDemandPeriod + ratchet_demand_period_start BIGINT, + ratchet_demand_period_duration BIGINT, + + -- Embedded SummaryMeasurement: overallConsumptionLastPeriod + overall_consumption_last_period_multiplier VARCHAR(255), + overall_consumption_last_period_timestamp BIGINT, + overall_consumption_last_period_uom VARCHAR(50), + overall_consumption_last_period_value BIGINT, + overall_consumption_last_period_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentBillingPeriodOverAllConsumption + current_billing_period_overall_consumption_multiplier VARCHAR(255), + current_billing_period_overall_consumption_timestamp BIGINT, + current_billing_period_overall_consumption_uom VARCHAR(50), + current_billing_period_overall_consumption_value BIGINT, + current_billing_period_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayLastYearNetConsumption + current_day_last_year_net_consumption_multiplier VARCHAR(255), + current_day_last_year_net_consumption_timestamp BIGINT, + current_day_last_year_net_consumption_uom VARCHAR(50), + current_day_last_year_net_consumption_value BIGINT, + current_day_last_year_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayNetConsumption + current_day_net_consumption_multiplier VARCHAR(255), + current_day_net_consumption_timestamp BIGINT, + current_day_net_consumption_uom VARCHAR(50), + current_day_net_consumption_value BIGINT, + current_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: currentDayOverallConsumption + current_day_overall_consumption_multiplier VARCHAR(255), + current_day_overall_consumption_timestamp BIGINT, + current_day_overall_consumption_uom VARCHAR(50), + current_day_overall_consumption_value BIGINT, + current_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: peakDemand + peak_demand_multiplier VARCHAR(255), + peak_demand_timestamp BIGINT, + peak_demand_uom VARCHAR(50), + peak_demand_value BIGINT, + peak_demand_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayLastYearOverallConsumption + previous_day_last_year_overall_consumption_multiplier VARCHAR(255), + previous_day_last_year_overall_consumption_timestamp BIGINT, + previous_day_last_year_overall_consumption_uom VARCHAR(50), + previous_day_last_year_overall_consumption_value BIGINT, + previous_day_last_year_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayNetConsumption + previous_day_net_consumption_multiplier VARCHAR(255), + previous_day_net_consumption_timestamp BIGINT, + previous_day_net_consumption_uom VARCHAR(50), + previous_day_net_consumption_value BIGINT, + previous_day_net_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: previousDayOverallConsumption + previous_day_overall_consumption_multiplier VARCHAR(255), + previous_day_overall_consumption_timestamp BIGINT, + previous_day_overall_consumption_uom VARCHAR(50), + previous_day_overall_consumption_value BIGINT, + previous_day_overall_consumption_reading_type_ref VARCHAR(512), + + -- Embedded SummaryMeasurement: ratchetDemand + ratchet_demand_multiplier VARCHAR(255), + ratchet_demand_timestamp BIGINT, + ratchet_demand_uom VARCHAR(50), + ratchet_demand_value BIGINT, + ratchet_demand_reading_type_ref VARCHAR(512), + + -- Foreign key relationships + usage_point_id CHAR(36), + + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE +); + +CREATE INDEX idx_usage_summary_usage_point_id ON usage_summaries (usage_point_id); +CREATE INDEX idx_usage_summary_billing_period_start ON usage_summaries (billing_period_start); +CREATE INDEX idx_usage_summary_created ON usage_summaries (created); +CREATE INDEX idx_usage_summary_updated ON usage_summaries (updated); + +-- Related Links Table for Usage Summaries +CREATE TABLE usage_summary_related_links +( + usage_summary_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE +); + +CREATE INDEX idx_usage_summary_related_links ON usage_summary_related_links (usage_summary_id); + +-- Line Item Table (Object-based entity, no IdentifiedObject) +-- LineItem extends Object per ESPI 4.0 XSD (espi.xsd:1449) +-- XSD sequence: amount → rounding → dateTime → note → measurement → itemKind → unitCost → itemPeriod +CREATE TABLE line_items +( + id BIGSERIAL PRIMARY KEY, + + -- ESPI 4.0 fields in XSD sequence order + amount BIGINT, + rounding BIGINT, + date_time BIGINT, + note VARCHAR(256) NOT NULL, + + -- Embedded SummaryMeasurement: measurement + measurement_multiplier VARCHAR(255), + measurement_timestamp BIGINT, + measurement_uom VARCHAR(50), + measurement_value BIGINT, + measurement_reading_type_ref VARCHAR(512), + + item_kind INTEGER NOT NULL, + unit_cost BIGINT, + + -- Embedded DateTimeInterval: itemPeriod + item_period_start BIGINT, + item_period_duration BIGINT, + + -- Foreign key relationship (parent: UsageSummary) + usage_summary_id CHAR(36), + + FOREIGN KEY (usage_summary_id) REFERENCES usage_summaries (id) ON DELETE CASCADE +); + +CREATE INDEX idx_line_item_usage_summary ON line_items (usage_summary_id); +CREATE INDEX idx_line_item_date_time ON line_items (date_time); +CREATE INDEX idx_line_item_amount ON line_items (amount); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepositoryTest.java index 6ea198dd..8e8939da 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/LineItemRepositoryTest.java @@ -18,10 +18,13 @@ package org.greenbuttonalliance.espi.common.repositories.usage; -import jakarta.validation.ConstraintViolation; +import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval; +import org.greenbuttonalliance.espi.common.domain.common.SummaryMeasurement; import org.greenbuttonalliance.espi.common.domain.usage.LineItemEntity; +import org.greenbuttonalliance.espi.common.domain.usage.UsagePointEntity; import org.greenbuttonalliance.espi.common.domain.usage.UsageSummaryEntity; 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; @@ -29,15 +32,13 @@ import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; /** * Comprehensive test suite for LineItemRepository. - * - * Tests all CRUD operations, 10 custom query methods, relationships, + * + * Tests all CRUD operations, 2 custom query methods, relationships, * and validation constraints for LineItem entities. */ @DisplayName("LineItem Repository Tests") @@ -49,6 +50,9 @@ class LineItemRepositoryTest extends BaseRepositoryTest { @Autowired private UsageSummaryRepository usageSummaryRepository; + @Autowired + private UsagePointRepository usagePointRepository; + @Nested @DisplayName("CRUD Operations") class CrudOperationsTest { @@ -57,11 +61,24 @@ class CrudOperationsTest { @DisplayName("Should save and retrieve line item successfully") void shouldSaveAndRetrieveLineItemSuccessfully() { // Arrange - LineItemEntity lineItem = new LineItemEntity( - 10000L, // $100.00 in cents - 1640995200L, // 2022-01-01 00:00:00 UTC - "Monthly service charge" + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); + + UsageSummaryEntity usageSummary = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L ); + usageSummary.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary = usageSummaryRepository.save(usageSummary); + + LineItemEntity lineItem = new LineItemEntity(); + lineItem.setAmount(10000L); + lineItem.setRounding(5L); + lineItem.setDateTime(1641000000L); + lineItem.setNote("Energy delivery charge"); + lineItem.setItemKind(2); // Energy Delivery Fee + lineItem.setUsageSummary(savedUsageSummary); // Act LineItemEntity saved = lineItemRepository.save(lineItem); @@ -73,58 +90,54 @@ void shouldSaveAndRetrieveLineItemSuccessfully() { assertThat(saved.getId()).isNotNull(); assertThat(retrieved).isPresent(); assertThat(retrieved.get().getAmount()).isEqualTo(10000L); - assertThat(retrieved.get().getDateTime()).isEqualTo(1640995200L); - assertThat(retrieved.get().getNote()).isEqualTo("Monthly service charge"); + assertThat(retrieved.get().getRounding()).isEqualTo(5L); + assertThat(retrieved.get().getDateTime()).isEqualTo(1641000000L); + assertThat(retrieved.get().getNote()).isEqualTo("Energy delivery charge"); + assertThat(retrieved.get().getItemKind()).isEqualTo(2); + assertThat(retrieved.get().getUsageSummary().getId()).isEqualTo(savedUsageSummary.getId()); } @Test - @DisplayName("Should save line item with rounding") - void shouldSaveLineItemWithRounding() { + @DisplayName("Should save line item with all optional fields") + void shouldSaveLineItemWithAllOptionalFields() { // Arrange - LineItemEntity lineItem = new LineItemEntity( - 9999L, // $99.99 in cents - 1L, // 1 cent rounding - 1640995200L, - "Usage charge with rounding" + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); + + UsageSummaryEntity usageSummary = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L ); + usageSummary.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary = usageSummaryRepository.save(usageSummary); - // Act - LineItemEntity saved = lineItemRepository.save(lineItem); - flushAndClear(); - Optional retrieved = lineItemRepository.findById(saved.getId()); + SummaryMeasurement measurement = new SummaryMeasurement("3", 1641000000L, "Wh", 15000L, null); + DateTimeInterval itemPeriod = new DateTimeInterval(1640995200L, 86400L); - // Assert - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getAmount()).isEqualTo(9999L); - assertThat(retrieved.get().getRounding()).isEqualTo(1L); - assertThat(retrieved.get().getNote()).isEqualTo("Usage charge with rounding"); - } - - @Test - @DisplayName("Should save line item with usage summary relationship") - void shouldSaveLineItemWithUsageSummaryRelationship() { - // Arrange - UsageSummaryEntity usageSummary = new UsageSummaryEntity(); - usageSummary.setDescription("Test Usage Summary"); - UsageSummaryEntity savedUsageSummary = usageSummaryRepository.saveAndFlush(usageSummary); - flushAndClear(); // Clear session to avoid cascade conflicts - - LineItemEntity lineItem = new LineItemEntity( - 5000L, // $50.00 in cents - 1640995200L, - "Line item with usage summary" - ); + LineItemEntity lineItem = new LineItemEntity(); + lineItem.setAmount(15000L); + lineItem.setRounding(10L); + lineItem.setDateTime(1641000000L); + lineItem.setNote("Peak usage charge"); + lineItem.setMeasurement(measurement); + lineItem.setItemKind(1); // Energy Generation Fee + lineItem.setUnitCost(150L); + lineItem.setItemPeriod(itemPeriod); lineItem.setUsageSummary(savedUsageSummary); // Act - LineItemEntity saved = lineItemRepository.saveAndFlush(lineItem); + LineItemEntity saved = lineItemRepository.save(lineItem); flushAndClear(); Optional retrieved = lineItemRepository.findById(saved.getId()); // Assert assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getUsageSummary()).isNotNull(); - assertThat(retrieved.get().getUsageSummary().getId()).isEqualTo(savedUsageSummary.getId()); + assertThat(retrieved.get().getMeasurement()).isNotNull(); + assertThat(retrieved.get().getMeasurement().getValue()).isEqualTo(15000L); + assertThat(retrieved.get().getUnitCost()).isEqualTo(150L); + assertThat(retrieved.get().getItemPeriod()).isNotNull(); + assertThat(retrieved.get().getItemPeriod().getStart()).isEqualTo(1640995200L); } } @@ -133,232 +146,209 @@ void shouldSaveLineItemWithUsageSummaryRelationship() { class CustomQueryMethodsTest { @Test - @DisplayName("Should find line items by usage summary ID") - void shouldFindLineItemsByUsageSummaryId() { + @DisplayName("Should find all IDs") + void shouldFindAllIds() { // Arrange - UsageSummaryEntity usageSummary = new UsageSummaryEntity(); - usageSummary.setDescription("Query Test Usage Summary"); - UsageSummaryEntity savedUsageSummary = usageSummaryRepository.saveAndFlush(usageSummary); - flushAndClear(); // Clear session to avoid cascade conflicts - - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "First item"); + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); + + UsageSummaryEntity usageSummary = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L + ); + usageSummary.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary = usageSummaryRepository.save(usageSummary); + + LineItemEntity lineItem1 = new LineItemEntity(10000L, 1641000000L, "Charge 1", 1); lineItem1.setUsageSummary(savedUsageSummary); - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1640995300L, "Second item"); + + LineItemEntity lineItem2 = new LineItemEntity(20000L, 1641086400L, "Charge 2", 2); lineItem2.setUsageSummary(savedUsageSummary); - - lineItemRepository.saveAndFlush(lineItem1); - lineItemRepository.saveAndFlush(lineItem2); + + LineItemEntity saved1 = lineItemRepository.save(lineItem1); + LineItemEntity saved2 = lineItemRepository.save(lineItem2); flushAndClear(); // Act - List lineItems = lineItemRepository.findByUsageSummaryId(savedUsageSummary.getId()); + List allIds = lineItemRepository.findAllIds(); // Assert - assertThat(lineItems).hasSize(2); - assertThat(lineItems).extracting(LineItemEntity::getNote) - .containsExactly("First item", "Second item"); // Should be ordered by dateTime + assertThat(allIds).contains(saved1.getId(), saved2.getId()); } @Test - @DisplayName("Should find line items by date time range") - void shouldFindLineItemsByDateTimeRange() { + @DisplayName("Should find all line items by usage summary ID") + void shouldFindAllLineItemsByUsageSummaryId() { // Arrange - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Item 1"); // 2022-01-01 - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1641081600L, "Item 2"); // 2022-01-02 - LineItemEntity lineItem3 = new LineItemEntity(3000L, 1641168000L, "Item 3"); // 2022-01-03 - - lineItemRepository.save(lineItem1); - lineItemRepository.save(lineItem2); - lineItemRepository.save(lineItem3); - flushAndClear(); + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); - // Act - List lineItems = lineItemRepository.findByDateTimeRange( - 1640995200L, // 2022-01-01 - 1641081600L // 2022-01-02 + UsageSummaryEntity usageSummary1 = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L ); + usageSummary1.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary1 = usageSummaryRepository.save(usageSummary1); - // Assert - assertThat(lineItems).hasSize(2); - assertThat(lineItems).extracting(LineItemEntity::getNote) - .containsExactly("Item 1", "Item 2"); - } + UsageSummaryEntity usageSummary2 = new UsageSummaryEntity( + new DateTimeInterval(1643587200L, 2592000L), + 600000L, + 300000L + ); + usageSummary2.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary2 = usageSummaryRepository.save(usageSummary2); - @Test - @DisplayName("Should find line items by amount range") - void shouldFindLineItemsByAmountRange() { - // Arrange - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Low amount"); - LineItemEntity lineItem2 = new LineItemEntity(5000L, 1640995200L, "Medium amount"); - LineItemEntity lineItem3 = new LineItemEntity(10000L, 1640995200L, "High amount"); - - lineItemRepository.save(lineItem1); - lineItemRepository.save(lineItem2); - lineItemRepository.save(lineItem3); - flushAndClear(); + LineItemEntity lineItem1 = new LineItemEntity(10000L, 1641000000L, "Summary 1 - Item 1", 1); + lineItem1.setUsageSummary(savedUsageSummary1); - // Act - List lineItems = lineItemRepository.findByAmountRange(2000L, 8000L); + LineItemEntity lineItem2 = new LineItemEntity(20000L, 1641086400L, "Summary 1 - Item 2", 2); + lineItem2.setUsageSummary(savedUsageSummary1); - // Assert - assertThat(lineItems).hasSize(1); - assertThat(lineItems.get(0).getNote()).isEqualTo("Medium amount"); - assertThat(lineItems.get(0).getAmount()).isEqualTo(5000L); - } + LineItemEntity lineItem3 = new LineItemEntity(30000L, 1643600000L, "Summary 2 - Item 1", 1); + lineItem3.setUsageSummary(savedUsageSummary2); - @Test - @DisplayName("Should find line items by note containing text") - void shouldFindLineItemsByNoteContaining() { - // Arrange - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Monthly service charge"); - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1640995200L, "Usage charge for electricity"); - LineItemEntity lineItem3 = new LineItemEntity(3000L, 1640995200L, "Tax and fees"); - lineItemRepository.save(lineItem1); lineItemRepository.save(lineItem2); lineItemRepository.save(lineItem3); flushAndClear(); // Act - List lineItems = lineItemRepository.findByNoteContaining("charge"); + List lineItems = lineItemRepository.findAllByUsageSummaryId(savedUsageSummary1.getId()); // Assert assertThat(lineItems).hasSize(2); assertThat(lineItems).extracting(LineItemEntity::getNote) - .contains("Monthly service charge", "Usage charge for electricity"); + .containsExactlyInAnyOrder("Summary 1 - Item 1", "Summary 1 - Item 2"); } + } + + @Nested + @DisplayName("JPA Relationships") + class RelationshipsTest { @Test - @DisplayName("Should find all IDs") - void shouldFindAllIds() { + @DisplayName("Should maintain usage summary relationship") + void shouldMaintainUsageSummaryRelationship() { // Arrange - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Item 1"); - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1640995200L, "Item 2"); - - LineItemEntity saved1 = lineItemRepository.save(lineItem1); - LineItemEntity saved2 = lineItemRepository.save(lineItem2); - flushAndClear(); - - // Act - List allIds = lineItemRepository.findAllIds(); + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); - // Assert - assertThat(allIds).contains(saved1.getId(), saved2.getId()); - } + UsageSummaryEntity usageSummary = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L + ); + usageSummary.setDescription("Test Summary"); + usageSummary.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary = usageSummaryRepository.save(usageSummary); - @Test - @DisplayName("Should sum amounts by usage summary") - void shouldSumAmountsByUsageSummary() { - // Arrange - UsageSummaryEntity usageSummary = new UsageSummaryEntity(); - usageSummary.setDescription("Sum Test Usage Summary"); - UsageSummaryEntity savedUsageSummary = usageSummaryRepository.saveAndFlush(usageSummary); - flushAndClear(); // Clear session to avoid cascade conflicts - - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Item 1"); - lineItem1.setUsageSummary(savedUsageSummary); - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1640995200L, "Item 2"); - lineItem2.setUsageSummary(savedUsageSummary); - LineItemEntity lineItem3 = new LineItemEntity(3000L, 1640995200L, "Item 3"); - lineItem3.setUsageSummary(savedUsageSummary); - - lineItemRepository.saveAndFlush(lineItem1); - lineItemRepository.saveAndFlush(lineItem2); - lineItemRepository.saveAndFlush(lineItem3); - flushAndClear(); + LineItemEntity lineItem = new LineItemEntity(10000L, 1641000000L, "Test Charge", 1); + lineItem.setUsageSummary(savedUsageSummary); // Act - Long totalAmount = lineItemRepository.sumAmountsByUsageSummary(savedUsageSummary.getId()); + LineItemEntity saved = lineItemRepository.save(lineItem); + flushAndClear(); + Optional retrieved = lineItemRepository.findById(saved.getId()); // Assert - assertThat(totalAmount).isEqualTo(6000L); // 1000 + 2000 + 3000 + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getUsageSummary()).isNotNull(); + assertThat(retrieved.get().getUsageSummary().getId()).isEqualTo(savedUsageSummary.getId()); + assertThat(retrieved.get().getUsageSummary().getDescription()).isEqualTo("Test Summary"); } + } + + @Nested + @DisplayName("Business Logic") + class BusinessLogicTest { @Test - @DisplayName("Should count line items by usage summary") - void shouldCountLineItemsByUsageSummary() { + @DisplayName("Should validate line item correctly") + void shouldValidateLineItemCorrectly() { // Arrange - UsageSummaryEntity usageSummary = new UsageSummaryEntity(); - usageSummary.setDescription("Count Test Usage Summary"); - UsageSummaryEntity savedUsageSummary = usageSummaryRepository.saveAndFlush(usageSummary); - flushAndClear(); // Clear session to avoid cascade conflicts - - LineItemEntity lineItem1 = new LineItemEntity(1000L, 1640995200L, "Item 1"); - lineItem1.setUsageSummary(savedUsageSummary); - LineItemEntity lineItem2 = new LineItemEntity(2000L, 1640995200L, "Item 2"); - lineItem2.setUsageSummary(savedUsageSummary); - - lineItemRepository.saveAndFlush(lineItem1); - lineItemRepository.saveAndFlush(lineItem2); - flushAndClear(); + LineItemEntity validLineItem = new LineItemEntity(10000L, 1641000000L, "Valid charge", 1); + LineItemEntity invalidLineItem1 = new LineItemEntity(10000L, 1641000000L, null, 1); // null note + LineItemEntity invalidLineItem2 = new LineItemEntity(10000L, 1641000000L, " ", 1); // blank note - // Act - Long count = lineItemRepository.countByUsageSummary(savedUsageSummary.getId()); - - // Assert - assertThat(count).isEqualTo(2L); + // Act & Assert + assertThat(validLineItem.isValid()).isTrue(); + assertThat(invalidLineItem1.isValid()).isFalse(); + assertThat(invalidLineItem2.isValid()).isFalse(); } - } - - @Nested - @DisplayName("Entity Validation") - class ValidationTest { @Test - @DisplayName("Should validate required fields") - void shouldValidateRequiredFields() { + @DisplayName("Should correctly identify charges and credits") + void shouldCorrectlyIdentifyChargesAndCredits() { // Arrange - LineItemEntity lineItem = new LineItemEntity(); - // Not setting required fields: amount, dateTime, note + LineItemEntity charge = new LineItemEntity(10000L, 1641000000L, "Charge", 1); + LineItemEntity credit = new LineItemEntity(-5000L, 1641000000L, "Credit", 3); + LineItemEntity zeroAmount = new LineItemEntity(0L, 1641000000L, "Zero", 1); // Act & Assert - Set> violations = validator.validate(lineItem); - assertThat(violations).hasSize(3); // amount, dateTime, note are required - - Set violationMessages = violations.stream() - .map(ConstraintViolation::getMessage) - .collect(java.util.stream.Collectors.toSet()); - - assertThat(violationMessages).contains( - "Amount cannot be null", - "Date time cannot be null", - "Note cannot be null" - ); + assertThat(charge.isCharge()).isTrue(); + assertThat(charge.isCredit()).isFalse(); + + assertThat(credit.isCharge()).isFalse(); + assertThat(credit.isCredit()).isTrue(); + + assertThat(zeroAmount.isCharge()).isFalse(); + assertThat(zeroAmount.isCredit()).isFalse(); + assertThat(zeroAmount.isZeroAmount()).isTrue(); } @Test - @DisplayName("Should validate note length constraint") - void shouldValidateNoteLengthConstraint() { + @DisplayName("Should calculate total amount with rounding") + void shouldCalculateTotalAmountWithRounding() { // Arrange - String longNote = "A".repeat(257); // Exceeds 256 character limit - LineItemEntity lineItem = new LineItemEntity(1000L, 1640995200L, longNote); + LineItemEntity withRounding = new LineItemEntity(10000L, 15L, 1641000000L, "With rounding", 1); + LineItemEntity withoutRounding = new LineItemEntity(10000L, 1641000000L, "Without rounding", 1); // Act & Assert - Set> violations = validator.validate(lineItem); - assertThat(violations).hasSize(1); - assertThat(violations.iterator().next().getMessage()) - .isEqualTo("Note cannot exceed 256 characters"); + assertThat(withRounding.getTotalAmount()).isEqualTo(10015L); + assertThat(withRounding.hasRounding()).isTrue(); + + assertThat(withoutRounding.getTotalAmount()).isEqualTo(10000L); + assertThat(withoutRounding.hasRounding()).isFalse(); } } @Nested - @DisplayName("Entity Functionality") - class EntityFunctionalityTest { + @DisplayName("Entity Persistence") + class EntityPersistenceTest { @Test - @DisplayName("Should persist with auto-generated UUID") - void shouldPersistWithAutoGeneratedUuid() { + @DisplayName("Should persist and retrieve line item") + void shouldPersistAndRetrieveLineItem() { // Arrange - LineItemEntity lineItem = new LineItemEntity(1000L, 1640995200L, "UUID test"); + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); + + UsageSummaryEntity usageSummary = new UsageSummaryEntity( + new DateTimeInterval(1640995200L, 2592000L), + 500000L, + 250000L + ); + usageSummary.setUsagePoint(savedUsagePoint); + UsageSummaryEntity savedUsageSummary = usageSummaryRepository.save(usageSummary); + + LineItemEntity lineItem = new LineItemEntity(10000L, 1641000000L, "Persistence Test", 1); + lineItem.setUsageSummary(savedUsageSummary); // Act LineItemEntity saved = lineItemRepository.save(lineItem); flushAndClear(); // Assert + // LineItem extends Object (not IdentifiedObject) in ESPI 4.0 XSD, + // so it has Long ID but no Atom links or timestamps assertThat(saved.getId()).isNotNull(); - assertThat(saved.getAmount()).isEqualTo(1000L); - assertThat(saved.getNote()).isEqualTo("UUID test"); + assertThat(saved.getAmount()).isEqualTo(10000L); + assertThat(saved.getDateTime()).isEqualTo(1641000000L); + assertThat(saved.getNote()).isEqualTo("Persistence Test"); + assertThat(saved.getItemKind()).isEqualTo(1); + assertThat(saved.getUsageSummary().getId()).isEqualTo(savedUsageSummary.getId()); } } -} \ No newline at end of file +} diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsageSummaryRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsageSummaryRepositoryTest.java index 34a5e4bd..18c375eb 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsageSummaryRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsageSummaryRepositoryTest.java @@ -472,11 +472,13 @@ void shouldManageLineItemCollectionCorrectly() { lineItem1.setAmount(1000L); lineItem1.setDateTime(randomOffsetDateTime().toEpochSecond()); lineItem1.setNote("Additional charge 1"); + lineItem1.setItemKind(1); // Energy Generation Fee LineItemEntity lineItem2 = new LineItemEntity(); lineItem2.setAmount(2000L); lineItem2.setDateTime(randomOffsetDateTime().toEpochSecond()); lineItem2.setNote("Additional charge 2"); + lineItem2.setItemKind(2); // Energy Delivery Fee // Act saved.addCostAdditionalDetailLastPeriod(lineItem1);