diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java index d5368eb08..4acf4b50a 100644 --- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java +++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/DefaultSnfInvoicingSystem.java @@ -33,6 +33,7 @@ import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.INSTRUCTIONS_ISSUED_KEY; import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OCPP_CHARGERS_KEY; import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_GROUPS_KEY; +import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_KEY; import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.META_AVAILABLE_CREDIT; import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.newItem; import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument; @@ -103,7 +104,7 @@ * Default implementation of {@link SnfInvoicingSystem}. * * @author matt - * @version 1.3 + * @version 1.4 */ public class DefaultSnfInvoicingSystem implements SnfInvoicingSystem, SnfTaxCodeResolver { @@ -144,6 +145,7 @@ public class DefaultSnfInvoicingSystem implements SnfInvoicingSystem, SnfTaxCode private String instructionsIssuedKey = INSTRUCTIONS_ISSUED_KEY; private String ocppChargersKey = OCPP_CHARGERS_KEY; private String oscpCapacityGroupsKey = OSCP_CAPACITY_GROUPS_KEY; + private String oscpCapacityKey = OSCP_CAPACITY_KEY; private String dnp3DataPointsKey = DNP3_DATA_POINTS_KEY; private String accountCreditKey = AccountBalance.ACCOUNT_CREDIT_KEY; private int deliveryTimeoutSecs = DEFAULT_DELIVERY_TIMEOUT; @@ -312,6 +314,15 @@ public SnfInvoice generateInvoice(Long userId, LocalDate startDate, LocalDate en } items.add(item); } + if ( usage.getOscpCapacity().compareTo(BigInteger.ZERO) > 0 ) { + SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, oscpCapacityKey, + new BigDecimal(usage.getOscpCapacity()), usage.getOscpCapacityCost()); + item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OSCP_CAPACITY_KEY)); + if ( !dryRun ) { + invoiceItemDao.save(item); + } + items.add(item); + } if ( usage.getDnp3DataPoints().compareTo(BigInteger.ZERO) > 0 ) { SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, dnp3DataPointsKey, new BigDecimal(usage.getDnp3DataPoints()), usage.getDnp3DataPointsCost()); @@ -756,6 +767,30 @@ public void setOscpCapacityGroupsKey(String oscpCapacityGroupsKey) { "oscpCapacityGroupsKey"); } + /** + * Get the item key for OSCP Capacity. + * + * @return the key, never {@literal null}; defaults to + * {@link NodeUsages#OSCP_CAPACITY_KEY} + * @since 1.4 + */ + public String getOscpCapacityKey() { + return oscpCapacityKey; + } + + /** + * Set the item key for OSCP Capacity. + * + * @param oscpCapacityKey + * the oscpCapacityKey to set + * @throws IllegalArgumentException + * if the argument is {@literal null} + * @since 1.4 + */ + public void setOscpCapacityKey(String oscpCapacityKey) { + this.oscpCapacityKey = requireNonNullArgument(oscpCapacityKey, "oscpCapacityKey"); + } + /** * Get the item key for DNP3 Data Points. * diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java index 74c08ed48..c9495a6e3 100644 --- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java +++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsage.java @@ -56,12 +56,12 @@ *

* * @author matt - * @version 2.4 + * @version 2.5 */ public class NodeUsage extends BasicLongEntity implements InvoiceUsageRecord, Differentiable, NodeUsages { - private static final long serialVersionUID = 5442178658317850821L; + private static final long serialVersionUID = 7976017078304093207L; /** * Comparator that sorts {@link NodeUsage} objects by {@code id} in @@ -76,6 +76,7 @@ public class NodeUsage extends BasicLongEntity private BigInteger instructionsIssued; private BigInteger ocppChargers; private BigInteger oscpCapacityGroups; + private BigInteger oscpCapacity; private BigInteger dnp3DataPoints; private final NodeUsageCost costs; private BigDecimal totalCost; @@ -86,6 +87,7 @@ public class NodeUsage extends BasicLongEntity private BigInteger[] instructionsIssuedTiers; private BigInteger[] ocppChargersTiers; private BigInteger[] oscpCapacityGroupsTiers; + private BigInteger[] oscpCapacityTiers; private BigInteger[] dnp3DataPointsTiers; private NodeUsageCost[] costsTiers; @@ -139,6 +141,7 @@ public NodeUsage(Long nodeId, Instant created) { setInstructionsIssued(BigInteger.ZERO); setOcppChargers(BigInteger.ZERO); setOscpCapacityGroups(BigInteger.ZERO); + setOscpCapacity(BigInteger.ZERO); setDnp3DataPoints(BigInteger.ZERO); setTotalCost(BigDecimal.ZERO); this.costs = new NodeUsageCost(); @@ -161,6 +164,10 @@ public String toString() { builder.append(datumDaysStored); builder.append(", instructionsIssued="); builder.append(instructionsIssued); + builder.append(", oscpCapacityGroups="); + builder.append(oscpCapacityGroups); + builder.append(", oscpCapacity="); + builder.append(oscpCapacity); builder.append(", datumPropertiesInCost="); builder.append(costs.getDatumPropertiesInCost()); builder.append(", datumOutCost="); @@ -173,6 +180,8 @@ public String toString() { builder.append(costs.getOcppChargersCost()); builder.append(", oscpCapacityGroupsCost="); builder.append(costs.getOscpCapacityGroupsCost()); + builder.append(", oscpCapacityCost="); + builder.append(costs.getOscpCapacityCost()); builder.append(", dnp3DataPointsCost="); builder.append(costs.getDnp3DataPointsCost()); builder.append(", totalCost="); @@ -206,6 +215,7 @@ public boolean isSameAs(NodeUsage other) { && Objects.equals(instructionsIssued, other.instructionsIssued) && Objects.equals(ocppChargers, other.ocppChargers) && Objects.equals(oscpCapacityGroups, other.oscpCapacityGroups) + && Objects.equals(oscpCapacity, other.oscpCapacity) && Objects.equals(dnp3DataPoints, other.dnp3DataPoints); // @formatter:on } @@ -554,6 +564,7 @@ private static List tiersCostBreakdown(BigInteger[] counts, NodeUsage *
  • {@link #INSTRUCTIONS_ISSUED_KEY}
  • *
  • {@link #OCPP_CHARGERS_KEY}
  • *
  • {@link #OSCP_CAPACITY_GROUPS_KEY}
  • + *
  • {@link #OSCP_CAPACITY_KEY}
  • *
  • {@link #DNP3_DATA_POINTS_KEY}
  • * * @@ -567,6 +578,7 @@ public Map> getTiersCostBreakdown() { result.put(INSTRUCTIONS_ISSUED_KEY, getInstructionsIssuedTiersCostBreakdown()); result.put(OCPP_CHARGERS_KEY, getOcppChargersTiersCostBreakdown()); result.put(OSCP_CAPACITY_GROUPS_KEY, getOscpCapacityGroupsTiersCostBreakdown()); + result.put(OSCP_CAPACITY_KEY, getOscpCapacityTiersCostBreakdown()); result.put(DNP3_DATA_POINTS_KEY, getDnp3DataPointsTiersCostBreakdown()); return result; } @@ -584,6 +596,7 @@ public Map> getTiersCostBreakdown() { *
  • {@link #INSTRUCTIONS_ISSUED_KEY}
  • *
  • {@link #OCPP_CHARGERS_KEY}
  • *
  • {@link #OSCP_CAPACITY_GROUPS_KEY}
  • + *
  • {@link #OSCP_CAPACITY_KEY}
  • *
  • {@link #DNP3_DATA_POINTS_KEY}
  • * * @@ -603,6 +616,8 @@ public Map getUsageInfo() { costs.getOcppChargersCost())); result.put(OSCP_CAPACITY_GROUPS_KEY, new UsageInfo(OSCP_CAPACITY_GROUPS_KEY, new BigDecimal(oscpCapacityGroups), costs.getOscpCapacityGroupsCost())); + result.put(OSCP_CAPACITY_KEY, new UsageInfo(OSCP_CAPACITY_KEY, new BigDecimal(oscpCapacity), + costs.getOscpCapacityCost())); result.put(DNP3_DATA_POINTS_KEY, new UsageInfo(DNP3_DATA_POINTS_KEY, new BigDecimal(dnp3DataPoints), costs.getDnp3DataPointsCost())); return result; @@ -1182,4 +1197,120 @@ public void setInstructionsIssuedCostTiers(BigDecimal[] instructionsIssuedCostTi costsTiers[i].setInstructionsIssuedCost(val); } } + + /** + * Get the OSCP capacity. + * + * @return the count + * @since 2.5 + */ + public BigInteger getOscpCapacity() { + return oscpCapacity; + } + + /** + * Set the count of OSCP capacity . + * + * @param oscpCapacity + * the count to set; if {@literal null} then {@literal 0} will be + * stored + * @since 2.5 + */ + public void setOscpCapacity(BigInteger oscpCapacity) { + if ( oscpCapacity == null ) { + oscpCapacity = BigInteger.ZERO; + } + this.oscpCapacity = oscpCapacity; + } + + /** + * Get the cost of OSCP capacity. + * + * @return the cost + * @since 2.5 + */ + public BigDecimal getOscpCapacityCost() { + return costs.getOscpCapacityCost(); + } + + /** + * Set the cost of OSCP capacity. + * + * @param oscpCapacityCost + * the cost to set + * @since 2.5 + */ + public void setOscpCapacityCost(BigDecimal oscpCapacityCost) { + costs.setOscpCapacityCost(oscpCapacityCost); + } + + /** + * Get the OSCP capacity tier cost breakdown. + * + * @return the costs, never {@literal null} + * @since 2.5 + */ + @JsonIgnore + public List getOscpCapacityTiersCostBreakdown() { + return tiersCostBreakdown(oscpCapacityTiers, costsTiers, NodeUsageCost::getOscpCapacityCost); + } + + /** + * Get the OSCP capacity, per tier. + * + * @return the counts + * @since 2.5 + */ + public BigInteger[] getOscpCapacityTiers() { + return oscpCapacityTiers; + } + + /** + * Set the OSCP capacity, per tier. + * + * @param oscpCapacityTiers + * the counts to set + * @since 2.5 + */ + public void setOscpCapacityTiers(BigInteger[] oscpCapacityTiers) { + this.oscpCapacityTiers = oscpCapacityTiers; + } + + /** + * Set the OSCP capacity, per tier, as decimals. + * + * @param oscpCapacityTiers + * the counts to set + * @since 2.5 + */ + public void setOscpCapacityTiersNumeric(BigDecimal[] oscpCapacityTiers) { + this.oscpCapacityTiers = decimalsToIntegers(oscpCapacityTiers); + } + + /** + * Get the cost of OSCP capacity, per tier. + * + * @return the cost + * @since 2.5 + */ + public BigDecimal[] getOscpCapacityCostTiers() { + return getTierCostValues(costsTiers, NodeUsageCost::getOscpCapacityCost); + } + + /** + * Set the cost of OSCP capacity, per tier. + * + * @param oscpCapacityCostTiers + * the costs to set + * @since 2.5 + */ + public void setOscpCapacityCostTiers(BigDecimal[] oscpCapacityCostTiers) { + prepCostsTiers(oscpCapacityCostTiers); + for ( int i = 0; i < costsTiers.length; i++ ) { + BigDecimal val = (oscpCapacityCostTiers != null && i < oscpCapacityCostTiers.length + ? oscpCapacityCostTiers[i] + : null); + costsTiers[i].setOscpCapacityCost(val); + } + } } diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java index e1ab2ff67..b8234c018 100644 --- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java +++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageCost.java @@ -22,6 +22,7 @@ package net.solarnetwork.central.user.billing.snf.domain; +import java.io.Serializable; import java.math.BigDecimal; import java.util.Objects; @@ -34,9 +35,11 @@ *

    * * @author matt - * @version 1.3 + * @version 1.4 */ -public class NodeUsageCost { +public class NodeUsageCost implements Serializable { + + private static final long serialVersionUID = 2764937975318244422L; private BigDecimal datumPropertiesInCost; private BigDecimal datumDaysStoredCost; @@ -44,6 +47,7 @@ public class NodeUsageCost { private BigDecimal instructionsIssuedCost; private BigDecimal ocppChargersCost; private BigDecimal oscpCapacityGroupsCost; + private BigDecimal oscpCapacityCost; private BigDecimal dnp3DataPointsCost; /** @@ -51,7 +55,7 @@ public class NodeUsageCost { */ public NodeUsageCost() { this(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO); + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); } /** @@ -159,6 +163,40 @@ public NodeUsageCost(String datumPropertiesInCost, String datumOutCost, String d new BigDecimal(dnp3DataPointsCost)); } + /** + * Constructor. + * + *

    + * This constructor converts all costs to {@link BigDecimal} values. + *

    + * + * @param datumPropertiesInCost + * the properties in cost + * @param datumOutCost + * the datum out cost + * @param datumDaysStoredCost + * the days stored cost + * @param instructionsIssuedCost + * the instructions issued code + * @param ocppChargersCost + * the OCPP Chargers cost + * @param oscpCapacityGroupsCost + * the OSCP Capacity Groups cost + * @param oscpCapacityCost + * the OSCP capacity cost + * @param dnp3DataPointsCost + * the DNP3 Data Points cost + * @since 1.4 + */ + public NodeUsageCost(String datumPropertiesInCost, String datumOutCost, String datumDaysStoredCost, + String instructionsIssuedCost, String ocppChargersCost, String oscpCapacityGroupsCost, + String oscpCapacityCost, String dnp3DataPointsCost) { + this(new BigDecimal(datumPropertiesInCost), new BigDecimal(datumOutCost), + new BigDecimal(datumDaysStoredCost), new BigDecimal(instructionsIssuedCost), + new BigDecimal(ocppChargersCost), new BigDecimal(oscpCapacityGroupsCost), + new BigDecimal(oscpCapacityCost), new BigDecimal(dnp3DataPointsCost)); + } + /** * Constructor. * @@ -253,10 +291,46 @@ public NodeUsageCost(BigDecimal datumPropertiesInCost, BigDecimal datumOutCost, setDnp3DataPointsCost(dnp3DataPointsCost); } + /** + * Constructor. + * + * @param datumPropertiesInCost + * the properties in cost + * @param datumOutCost + * the datum out cost + * @param datumDaysStoredCost + * the days stored cost + * @param instructionsIssuedCost + * the instructions issued cost + * @param ocppChargersCost + * the OCPP Chargers cost + * @param oscpCapacityGroupsCost + * the OSCP Capacity Groups cost + * @param oscpCapacityCost + * the OSCP capacity cost + * @param dnp3DataPointsCost + * the DNP3 Data Points cost + * @since 1.4 + */ + public NodeUsageCost(BigDecimal datumPropertiesInCost, BigDecimal datumOutCost, + BigDecimal datumDaysStoredCost, BigDecimal instructionsIssuedCost, + BigDecimal ocppChargersCost, BigDecimal oscpCapacityGroupsCost, BigDecimal oscpCapacityCost, + BigDecimal dnp3DataPointsCost) { + super(); + setDatumPropertiesInCost(datumPropertiesInCost); + setDatumOutCost(datumOutCost); + setDatumDaysStoredCost(datumDaysStoredCost); + setInstructionsIssuedCost(instructionsIssuedCost); + setOcppChargersCost(ocppChargersCost); + setOscpCapacityGroupsCost(oscpCapacityGroupsCost); + setOscpCapacityCost(oscpCapacityCost); + setDnp3DataPointsCost(dnp3DataPointsCost); + } + @Override public int hashCode() { return Objects.hash(datumDaysStoredCost, datumOutCost, datumPropertiesInCost, ocppChargersCost, - oscpCapacityGroupsCost, dnp3DataPointsCost); + oscpCapacityGroupsCost, oscpCapacityCost, dnp3DataPointsCost); } @Override @@ -273,6 +347,7 @@ public boolean equals(Object obj) { && Objects.equals(datumPropertiesInCost, other.datumPropertiesInCost) && Objects.equals(ocppChargersCost, other.ocppChargersCost) && Objects.equals(oscpCapacityGroupsCost, other.oscpCapacityGroupsCost) + && Objects.equals(oscpCapacityCost, other.oscpCapacityCost) && Objects.equals(dnp3DataPointsCost, other.dnp3DataPointsCost); } @@ -290,6 +365,8 @@ public String toString() { builder.append(ocppChargersCost); builder.append(", oscpCapacityGroupsCost="); builder.append(oscpCapacityGroupsCost); + builder.append(", oscpCapacityCost="); + builder.append(oscpCapacityCost); builder.append(", dnp3DataPointsCost="); builder.append(dnp3DataPointsCost); builder.append("}"); @@ -425,6 +502,30 @@ public void setOscpCapacityGroupsCost(BigDecimal oscpCapacityGroupsCost) { this.oscpCapacityGroupsCost = oscpCapacityGroupsCost; } + /** + * Get the OSCP capacity cost. + * + * @return the cost, never {@literal null} + * @since 1.4 + */ + public BigDecimal getOscpCapacityCost() { + return oscpCapacityCost; + } + + /** + * Set the OSCP capacity cost. + * + * @param oscpCapacityCost + * the cost to set + * @since 1.4 + */ + public void setOscpCapacityCost(BigDecimal oscpCapacityCost) { + if ( oscpCapacityCost == null ) { + oscpCapacityCost = BigDecimal.ZERO; + } + this.oscpCapacityCost = oscpCapacityCost; + } + /** * Get the DNP3 Data Points cost. * diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java index 7d3ebdff5..4e604672b 100644 --- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java +++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsageType.java @@ -49,8 +49,11 @@ public enum NodeUsageType implements NodeUsages { /** OSCP Capacity Group usage. */ OscpCapacityGroups(6, OSCP_CAPACITY_GROUPS_KEY), + /** OSCP Capacity usage. */ + OscpCapacity(7, OSCP_CAPACITY_KEY), + /** DNP3 Data Points usage. */ - Dnp3DataPoints(7, DNP3_DATA_POINTS_KEY), + Dnp3DataPoints(8, DNP3_DATA_POINTS_KEY), ; @@ -97,6 +100,7 @@ public static NodeUsageType forKey(String key) { case INSTRUCTIONS_ISSUED_KEY -> InstructionsIssued; case OCPP_CHARGERS_KEY -> OcppChargers; case OSCP_CAPACITY_GROUPS_KEY -> OscpCapacityGroups; + case OSCP_CAPACITY_KEY -> OscpCapacity; case DNP3_DATA_POINTS_KEY -> Dnp3DataPoints; default -> throw new IllegalArgumentException("Unknown NodeUsageType key value: " + key); }; diff --git a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java index fe7471c59..749dfd839 100644 --- a/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java +++ b/solarnet/user-billing/src/main/java/net/solarnetwork/central/user/billing/snf/domain/NodeUsages.java @@ -26,7 +26,7 @@ * Node usage constants. * * @author matt - * @version 1.3 + * @version 1.4 */ public interface NodeUsages { @@ -48,6 +48,9 @@ public interface NodeUsages { /** A key to use for OSCP Capacity Group usage. */ String OSCP_CAPACITY_GROUPS_KEY = "oscp-cap-groups"; + /** A key to use for OSCP capacity usage. */ + String OSCP_CAPACITY_KEY = "oscp-cap"; + /** A key to use for DNP3 data points usage. */ String DNP3_DATA_POINTS_KEY = "dnp3-data-points"; diff --git a/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml b/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml index d62f3885b..ea7ce13ae 100644 --- a/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml +++ b/solarnet/user-billing/src/main/resources/net/solarnetwork/central/user/billing/snf/dao/mybatis/map/NodeUsage.xml @@ -33,6 +33,7 @@ , nu.instr_issued AS node_usage_instr_issued , nu.ocpp_chargers AS node_usage_ocpp_chargers , nu.oscp_cap_groups AS node_usage_oscp_cap_groups + , nu.oscp_cap AS node_usage_oscp_cap , nu.dnp3_data_points AS node_usage_dnp3_data_points , nu.prop_in_tiers AS node_usage_prop_in_tiers @@ -41,6 +42,7 @@ , nu.instr_issued_tiers AS node_usage_instr_issued_tiers , nu.ocpp_chargers_tiers AS node_usage_ocpp_chargers_tiers , nu.oscp_cap_groups_tiers AS node_usage_oscp_cap_groups_tiers + , nu.oscp_cap_tiers AS node_usage_oscp_cap_tiers , nu.dnp3_data_points_tiers AS node_usage_dnp3_data_points_tiers @@ -51,6 +53,7 @@ + @@ -59,6 +62,7 @@ + @@ -70,6 +74,7 @@ , nu.instr_issued_cost AS node_usage_instr_issued_cost , nu.ocpp_chargers_cost AS node_usage_ocpp_chargers_cost , nu.oscp_cap_groups_cost AS node_usage_oscp_cap_groups_cost + , nu.oscp_cap_cost AS node_usage_oscp_cap_cost , nu.dnp3_data_points_cost AS node_usage_dnp3_data_points_cost , nu.total_cost AS node_usage_total_cost @@ -79,6 +84,7 @@ , nu.instr_issued_tiers_cost AS node_usage_instr_issued_tiers_cost , nu.ocpp_chargers_tiers_cost AS node_usage_ocpp_chargers_tiers_cost , nu.oscp_cap_groups_tiers_cost AS node_usage_oscp_cap_groups_tiers_cost + , nu.oscp_cap_tiers_cost AS node_usage_oscp_cap_tiers_cost , nu.dnp3_data_points_tiers_cost AS node_usage_dnp3_data_points_tiers_cost @@ -89,6 +95,7 @@ + @@ -98,6 +105,7 @@ + diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java index 8e2ecdb73..a0e709a02 100644 --- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java +++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/AbstractMyBatisDaoTestSupport.java @@ -26,7 +26,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import java.math.BigDecimal; +import java.sql.Array; import java.sql.Timestamp; +import java.sql.Types; import java.time.Instant; import java.util.List; import java.util.Map; @@ -39,6 +41,7 @@ import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.jdbc.core.PreparedStatementCallback; import org.springframework.test.context.ContextConfiguration; import net.solarnetwork.central.test.AbstractCentralTransactionalTest; import net.solarnetwork.central.user.billing.snf.domain.Account; @@ -196,6 +199,32 @@ protected UUID setupDatumStream(Long nodeId, String sourceId) { return streamId; } + protected UUID setupDatumStream(Long nodeId, String sourceId, String iProps[], String aProps[]) { + jdbcTemplate.execute(""" + INSERT INTO solardatm.da_datm_meta (stream_id, node_id, source_id, names_i, names_a) + VALUES (?,?,?,?,?) + ON CONFLICT (node_id, source_id) DO NOTHING + """, (PreparedStatementCallback) ps -> { + ps.setObject(1, UUID.randomUUID()); + ps.setObject(2, nodeId, Types.BIGINT); + ps.setString(3, sourceId); + + Array iPropsArray = ps.getConnection().createArrayOf("text", iProps); + ps.setArray(4, iPropsArray); + iPropsArray.free(); + + Array aPropsArray = ps.getConnection().createArrayOf("text", aProps); + ps.setArray(5, aPropsArray); + aPropsArray.free(); + ps.execute(); + return null; + }); + UUID streamId = jdbcTemplate.queryForObject( + "select stream_id from solardatm.da_datm_meta where node_id = ? and source_id = ?", + UUID.class, nodeId, sourceId); + return streamId; + } + protected UUID addAuditDatumMonthly(Long nodeId, String sourceId, Instant date, long propCount, long datumQueryCount, int datumCount, short datumHourlyCount, short datumDailyCount, boolean monthPresent) { @@ -217,6 +246,37 @@ protected void addAuditAccumulatingDatumDaily(Long nodeId, String sourceId, Inst datumCount, datumHourlyCount, datumDailyCount, datumMonthlyCount); } + protected void addAggregateDatumDaily(UUID streamId, Instant date, BigDecimal[] iData, + BigDecimal[][] iStats, BigDecimal[] aData, BigDecimal[][] aStats) { + jdbcTemplate.execute(""" + INSERT INTO solardatm.agg_datm_daily + (stream_id, ts_start, data_i, stat_i, data_a, read_a) + VALUES (?,?,?,?,?,?) + """, (PreparedStatementCallback) ps -> { + ps.setObject(1, streamId); + ps.setTimestamp(2, Timestamp.from(date)); + + Array iDataArray = ps.getConnection().createArrayOf("numeric", iData); + ps.setArray(3, iDataArray); + iDataArray.free(); + + Array iStatsArray = ps.getConnection().createArrayOf("numeric", iStats); + ps.setArray(4, iStatsArray); + iStatsArray.free(); + + Array aDataArray = ps.getConnection().createArrayOf("numeric", aData); + ps.setArray(5, aDataArray); + aDataArray.free(); + + Array aStatsArray = ps.getConnection().createArrayOf("numeric", aStats); + ps.setArray(6, aStatsArray); + aStatsArray.free(); + + ps.execute(); + return null; + }); + } + protected void assertAccountBalance(Long accountId, BigDecimal chargeTotal, BigDecimal paymentTotal) { getSqlSessionTemplate().flushStatements(); diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java index 34471992c..5104d1f40 100644 --- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java +++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/dao/mybatis/test/MyBatisNodeUsageDaoTests.java @@ -40,6 +40,8 @@ import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @@ -1053,4 +1055,207 @@ public void usageForUser_oneNodeOneSource_withInstructions() { } } + @Test + public void usageForUser_oneNodeOneSource_withOscpCap() { + // GIVEN + final LocalDate month = LocalDate.of(2024, 4, 1); + final String sourceId = "S1"; + setupDatumStream(nodeId, sourceId); + + // add 10 days worth of audit data + final int numDays = 10; + for ( int dayOffset = 0; dayOffset < numDays; dayOffset++ ) { + Instant day = month.plusDays(dayOffset).atStartOfDay(TEST_ZONE).toInstant(); + addAuditAccumulatingDatumDaily(nodeId, sourceId, day, 1000000, 2000000, 3000000, 4000000); + addAuditDatumMonthly(nodeId, sourceId, day, 100000, 200000, 300000, (short) 400000, + (short) 500000, true); + } + + final Long fpId = OscpTestUtils.saveFlexibilityProviderAuthId(jdbcTemplate, userId, + randomUUID().toString()); + final Long cpId = OscpTestUtils.saveCapacityProvider(jdbcTemplate, userId, fpId, "CP"); + final Long coId = OscpTestUtils.saveCapacityOptimizer(jdbcTemplate, userId, fpId, "CO"); + final int numOscpCapacityGroups = 150; + final int numOscpFlexibilityAssets = 1; + final String[] iProps = new String[] { "watts" }; + final String[] eProps = new String[] { "wattHours" }; + final Instant startDay = month.atStartOfDay(ZoneOffset.UTC).toInstant(); + for ( int i = 0; i < numOscpCapacityGroups; i++ ) { + Long cgId = OscpTestUtils.saveCapacityGroup(jdbcTemplate, userId, "CG-%d".formatted(i), + "CG-%d".formatted(i), cpId, coId); + for ( int j = 0; j < numOscpFlexibilityAssets; j++ ) { + String faIdent = "CG-%d Asset-%d".formatted(i, j); + + UUID streamId = setupDatumStream(nodeId, faIdent, iProps, eProps); + + BigDecimal energy = BigDecimal.ZERO; + for ( int t = 0; t < numDays; t++ ) { + BigDecimal power = new BigDecimal(1_000_000L * (t + 1)); + BigDecimal[] iData = new BigDecimal[] { power }; + BigDecimal[][] iStats = new BigDecimal[][] { + // count, min, max + new BigDecimal[] { new BigDecimal(10L), new BigDecimal(0L), power } }; + + BigDecimal[] aData = new BigDecimal[] { new BigDecimal(1_000_000L * (t + 1)) }; + BigDecimal end = energy.add(power); + BigDecimal[][] aStats = new BigDecimal[][] { + // diff, start, end + new BigDecimal[] { power, energy, end } }; + addAggregateDatumDaily(streamId, startDay.plus(t, ChronoUnit.DAYS), iData, iStats, + aData, aStats); + energy = end; + } + + OscpTestUtils.saveFlexibilityAsset(jdbcTemplate, userId, faIdent, faIdent, cgId, nodeId, + faIdent, iProps, eProps); + } + } + + debugRows("solardatm.aud_acc_datm_daily", "ts_start"); + debugRows("solardatm.agg_datm_daily", "stream_id,ts_start"); + debugQuery(format( + "select * from solarbill.billing_usage_tier_details(%d, '2024-04-01'::timestamp, '2024-05-01'::timestamp, '2024-04-01'::date)", + userId)); + + debugQuery(""" + SELECT COUNT(*) AS oscp_cg_count + FROM solaroscp.oscp_cg_conf WHERE user_id = %d AND enabled = TRUE + """.formatted(userId)); + + // WHEN + UsageTiers tiers = dao.effectiveUsageTiers(month); + Map> tierMap = tiers.tierMap(); + + List r1 = dao.findNodeUsageForAccount(userId, month, month.plusMonths(1)); + List r2 = dao.findUsageForAccount(userId, month, month.plusMonths(1)); + + // THEN + int i = 0; + for ( List results : Arrays.asList(r1, r2) ) { + assertThat("Results non-null with single result", results, hasSize(1)); + NodeUsage usage = results.get(0); + if ( i == 0 ) { + assertThat("Node ID present for node-level usage", usage.getId(), equalTo(nodeId)); + assertThat("Node usage description is node name", usage.getDescription(), + equalTo(format("Test Node %d", nodeId))); + } else { + assertThat("No node ID for account-level usage", usage.getId(), nullValue()); + } + assertThat("Properties in count aggregated", usage.getDatumPropertiesIn(), + equalTo(BigInteger.valueOf(100000L * numDays))); + assertThat("Datum out count aggregated", usage.getDatumOut(), + equalTo(BigInteger.valueOf(200000L * numDays))); + assertThat("Datum stored count aggregated", usage.getDatumDaysStored(), + equalTo(BigInteger.valueOf((1000000L + 2000000L + 3000000L + 4000000L) * numDays))); + + // see tiersForDate_202211 + Map> tiersBreakdown = usage.getTiersCostBreakdown(); + List propsInTiersCost = tiersBreakdown.get(NodeUsage.DATUM_PROPS_IN_KEY); + assertThat("Properties in cost tier count", propsInTiersCost, hasSize(2)); + List datumOutTiersCost = tiersBreakdown.get(NodeUsage.DATUM_OUT_KEY); + assertThat("Datum out cost tier count", datumOutTiersCost, hasSize(1)); + List datumStoredTiersCost = tiersBreakdown.get(NodeUsage.DATUM_DAYS_STORED_KEY); + assertThat("Datum stored cost tier count", datumStoredTiersCost, hasSize(2)); + + if ( i == 0 ) { + List ocppChargersTiersCost = tiersBreakdown.get(NodeUsage.OCPP_CHARGERS_KEY); + assertThat("No node-level OCPP charger costs", ocppChargersTiersCost, hasSize(0)); + List oscpCapacityGroupsTiersCost = tiersBreakdown + .get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY); + assertThat("No node-level OSCP capacity group costs", oscpCapacityGroupsTiersCost, + hasSize(0)); + List dnp3DataPointsTiersCost = tiersBreakdown + .get(NodeUsage.DNP3_DATA_POINTS_KEY); + assertThat("No node-level DNP3 Data Points costs", dnp3DataPointsTiersCost, hasSize(0)); + } else { + List ocppChargersTiersCost = tiersBreakdown.get(NodeUsage.OCPP_CHARGERS_KEY); + assertThat("No account-level OCPP charger costs", ocppChargersTiersCost, hasSize(0)); + List oscpCapacityGroupsTiersCost = tiersBreakdown + .get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY); + assertThat("Account-level OSCP capacity group costs", oscpCapacityGroupsTiersCost, + hasSize(2)); + List oscpCapacityTiersCost = tiersBreakdown.get(NodeUsage.OSCP_CAPACITY_KEY); + assertThat("Account-level OSCP capacity costs", oscpCapacityTiersCost, hasSize(4)); + List dnp3DataPointsTiersCost = tiersBreakdown + .get(NodeUsage.DNP3_DATA_POINTS_KEY); + assertThat("No account-level DNP3 Data Points costs", dnp3DataPointsTiersCost, + hasSize(0)); + /*- + datum-props-in=[ + NamedCost{name=Tier 1, quantity=500000, cost=2.500000}, + NamedCost{name=Tier 2, quantity=500000, cost=1.500000}], + datum-out=[ + NamedCost{name=Tier 1, quantity=2000000, cost=0.2000000}], + datum-days-stored=[ + NamedCost{name=Tier 1, quantity=10000000, cost=0.50000000}, + NamedCost{name=Tier 2, quantity=90000000, cost=0.90000000}], + ocpp-chargers=[ + NamedCost{name=Tier 1, quantity=250, cost=500}, + NamedCost{name=Tier 2, quantity=1750, cost=1750}], + oscp-cap-groups=[ + NamedCost{name=Tier 1, quantity=100, cost=200}, + NamedCost{name=Tier 2, quantity=50, cost=75.0}] + oscp-cap=[ + NamedCost{name=Tier 1, quantity=6000000, cost=180.000000} + NamedCost{name=Tier 2, quantity=34000000, cost=850.00000}, + NamedCost{name=Tier 3, quantity=60000000, cost=1050.000000}, + NamedCost{name=Tier 4, quantity=1400000000, cost=14000.00000}] + */ + // @formatter:off + assertThat("Properties in cost", usage.getDatumPropertiesInCost().setScale(3), equalTo( + new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(0).getCost()) + .add( new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(1).getCost())) + .setScale(3) + )); + assertThat("Properties in cost tiers", propsInTiersCost, contains( + NamedCost.forTier(1, "500000", new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(0).getCost()).toString()), + NamedCost.forTier(2, "500000", new BigDecimal("500000").multiply(tierMap.get(NodeUsage.DATUM_PROPS_IN_KEY).get(1).getCost()).toString()))); + + assertThat("Datum out cost", usage.getDatumOutCost().setScale(3), equalTo( + new BigDecimal("2000000").multiply(tierMap.get(NodeUsage.DATUM_OUT_KEY).get(0).getCost()) + .setScale(3) + )); + assertThat("Datum out cost tiers", datumOutTiersCost, contains( + NamedCost.forTier(4, "2000000", new BigDecimal("2000000").multiply(tierMap.get(NodeUsage.DATUM_OUT_KEY).get(0).getCost()).toString()))); + + assertThat("Datum stored cost", usage.getDatumDaysStoredCost().setScale(3), equalTo( + new BigDecimal("10000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(0).getCost()) + .add( new BigDecimal("90000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(1).getCost())) + .setScale(3) + )); + assertThat("Datum stored cost tiers", datumStoredTiersCost, contains( + NamedCost.forTier(1, "10000000", new BigDecimal("10000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(0).getCost()).toString()), + NamedCost.forTier(2, "90000000", new BigDecimal("90000000").multiply(tierMap.get(NodeUsage.DATUM_DAYS_STORED_KEY).get(1).getCost()).toString()))); + + assertThat("OSCP Capacity Groups cost", usage.getOscpCapacityGroupsCost().setScale(3), equalTo( + new BigDecimal("100") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(0).getCost()) + .add( new BigDecimal("50") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(1).getCost())) + .setScale(3) + )); + assertThat("OSCP Capacity Groups cost tiers", oscpCapacityGroupsTiersCost, contains( + NamedCost.forTier(1, "100", new BigDecimal("100") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(0).getCost()).toString()), + NamedCost.forTier(2, "50", new BigDecimal("50") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_GROUPS_KEY).get(1).getCost()).toString()) + )); + + assertThat("OSCP Capacity cost", usage.getOscpCapacityCost().setScale(3), equalTo( + new BigDecimal("6000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(0).getCost()) + .add( new BigDecimal("34000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(1).getCost())) + .add( new BigDecimal("60000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(2).getCost())) + .add( new BigDecimal("1400000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(3).getCost())) + .setScale(3) + )); + assertThat("OSCP Capacity cost tiers", oscpCapacityTiersCost, contains( + NamedCost.forTier(1, "6000000", new BigDecimal("6000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(0).getCost()).toString()), + NamedCost.forTier(2, "34000000", new BigDecimal("34000000") .multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(1).getCost()).toString()), + NamedCost.forTier(3, "60000000", new BigDecimal("60000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(2).getCost()).toString()), + NamedCost.forTier(4, "1400000000", new BigDecimal("1400000000").multiply(tierMap.get(NodeUsage.OSCP_CAPACITY_KEY).get(3).getCost()).toString()) + )); + + // @formatter:on + } + i++; + } + + } + } diff --git a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java index f1cd99821..6514a45fa 100644 --- a/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java +++ b/solarnet/user-billing/src/test/java/net/solarnetwork/central/user/billing/snf/test/OscpTestUtils.java @@ -22,6 +22,8 @@ package net.solarnetwork.central.user.billing.snf.test; +import java.math.BigDecimal; +import java.sql.Array; import java.sql.PreparedStatement; import java.sql.Statement; import java.sql.Types; @@ -32,7 +34,7 @@ * Testing utilities for Flexibility Provider. * * @author matt - * @version 1.0 + * @version 1.1 */ public final class OscpTestUtils { @@ -164,4 +166,75 @@ public static Long saveCapacityGroup(JdbcOperations jdbcOps, Long userId, String return holder.getKeyAs(Long.class); } + /** + * Save a new Capacity Optimizer. + * + * @param jdbcOps + * the JDBC template to use + * @param userId + * the user ID + * @param name + * the display name + * @param ident + * the unique idnentifier + * @param cgId + * the Capacity Group ID + * @param nodeId + * the node ID + * @param sourceId + * the source ID + * @param iProps + * the instantaneous properties + * @param eProps + * the accumulating properties + * @return the new ID + * @since 1.1 + */ + public static Long saveFlexibilityAsset(JdbcOperations jdbcOps, Long userId, String name, + String ident, Long cgId, Long nodeId, String sourceId, String[] iProps, String[] eProps) { + GeneratedKeyHolder holder = new GeneratedKeyHolder(); + jdbcOps.update((con) -> { + PreparedStatement stmt = con.prepareStatement(""" + INSERT INTO solaroscp.oscp_asset_conf ( + user_id, enabled, cname, ident, cg_id, node_id, source_id + , iprops, iprops_unit, iprops_mult + , eprops, eprops_unit, eprops_mult + , audience, category) + VALUES ( + ?, ?, ?, ?, ?, ?, ? + , ?, ?, ? + , ?, ?, ? + , ?, ? + ) RETURNING id + """, Statement.RETURN_GENERATED_KEYS); + stmt.setObject(1, userId, Types.BIGINT); + stmt.setBoolean(2, true); + stmt.setString(3, name); + stmt.setString(4, ident); + stmt.setObject(5, cgId, Types.BIGINT); + stmt.setObject(6, nodeId, Types.BIGINT); + stmt.setString(7, sourceId); + + Array iPropsArray = con.createArrayOf("text", iProps); + stmt.setArray(8, iPropsArray); + iPropsArray.free(); + + stmt.setInt(9, 'P'); // kW + stmt.setBigDecimal(10, new BigDecimal("0.001")); + + Array ePropsArray = con.createArrayOf("text", eProps); + stmt.setArray(11, ePropsArray); + ePropsArray.free(); + + stmt.setInt(12, 'E'); // kWh + stmt.setBigDecimal(13, new BigDecimal("0.001")); + + stmt.setInt(14, 'o'); // Capacity Optimizer + stmt.setInt(15, 'v'); // Charging + + return stmt; + }, holder); + return holder.getKeyAs(Long.class); + } + }