Skip to content

Commit 8fa8e05

Browse files
authored
CDA-75 - Adds flow-unit support to CDA for pump accounting. (#1539)
fixes #1180 Note this allows things to work with CDA converting to SI prior to store - a more permanent fix would be in the db to include native-units to the object holding the flow
1 parent 535fb8a commit 8fa8e05

File tree

13 files changed

+366
-46
lines changed

13 files changed

+366
-46
lines changed

cda-gui/package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCreateController.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
import cwms.cda.data.dto.watersupply.WaterSupplyAccounting;
4646
import cwms.cda.formatters.ContentType;
4747
import cwms.cda.formatters.Formats;
48+
import cwms.cda.data.dao.watersupply.WaterSupplyUtils;
49+
import mil.army.usace.hec.metadata.DataSetIllegalArgumentException;
4850
import io.javalin.core.util.Header;
4951
import io.javalin.http.Context;
5052
import io.javalin.http.Handler;
@@ -132,9 +134,15 @@ public void handle(@NotNull Context ctx) {
132134
}
133135
}
134136

135-
waterSupplyAccountingDao.storeAccounting(accounting);
136-
StatusResponse re = new StatusResponse(office, "The pump accounting entry was created.", contractId);
137-
ctx.status(HttpServletResponse.SC_CREATED).json(re);
137+
// Ensure flows are stored in SI units
138+
try {
139+
WaterSupplyAccounting accountingInSi = WaterSupplyUtils.convertAccountingFlowsToSi(accounting);
140+
waterSupplyAccountingDao.storeAccounting(accountingInSi);
141+
StatusResponse re = new StatusResponse(office, "The pump accounting entry was created.", contractId);
142+
ctx.status(HttpServletResponse.SC_CREATED).json(re);
143+
} catch (DataSetIllegalArgumentException | IllegalArgumentException ex) {
144+
ctx.status(HttpServletResponse.SC_BAD_REQUEST).json("Unable to process units for flow: " + ex.getMessage());
145+
}
138146
}
139147
}
140148

@@ -146,4 +154,6 @@ private boolean searchForTransferType(PumpTransfer accounting, List<LookupType>
146154
}
147155
return false;
148156
}
157+
158+
149159
}

cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDao.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public List<WaterSupplyAccounting> retrieveAccounting(String contractName, Water
9090
contractRefT, units, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag,
9191
endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType);
9292
if (!watUsrContractAcctObjTs.isEmpty()) {
93-
return WaterSupplyUtils.toWaterSupplyAccountingList(c, watUsrContractAcctObjTs);
93+
return WaterSupplyUtils.toWaterSupplyAccountingList(c, watUsrContractAcctObjTs, units);
9494
} else {
9595
return new ArrayList<>();
9696
}

cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyUtils.java

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
import java.util.Map;
4848
import java.util.TreeMap;
4949
import com.google.common.flogger.FluentLogger;
50+
import mil.army.usace.hec.metadata.DataSetIllegalArgumentException;
51+
import mil.army.usace.hec.metadata.Parameter;
52+
import mil.army.usace.hec.metadata.UnitUtil;
53+
import mil.army.usace.hec.metadata.UnitsConversionException;
5054
import org.jetbrains.annotations.NotNull;
5155
import org.jooq.impl.DSL;
5256
import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T;
@@ -62,13 +66,56 @@
6266
import usace.cwms.db.jooq.codegen.udt.records.WAT_USR_CONTRACT_ACCT_TAB_T;
6367

6468

65-
final class WaterSupplyUtils {
69+
public final class WaterSupplyUtils {
6670
private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass();
6771

6872
private WaterSupplyUtils() {
6973
throw new IllegalStateException("Utility class");
7074
}
7175

76+
/**
77+
* Converts all PumpTransfer flow values to SI units and returns a new WaterSupplyAccounting instance.
78+
* The SI flow units are determined using metadata for the FLOW parameter. If any conversion fails,
79+
* an IllegalArgumentException is thrown wrapping the UnitsConversionException. If the SI units
80+
* cannot be determined due to metadata issues, a DataSetIllegalArgumentException may be thrown.
81+
*
82+
* @param accounting the input WaterSupplyAccounting, possibly containing non-SI flow units
83+
* @return a new WaterSupplyAccounting with all flows in SI units
84+
* @throws DataSetIllegalArgumentException if SI flow units cannot be determined
85+
* @throws IllegalArgumentException if a units conversion fails
86+
*/
87+
public static WaterSupplyAccounting convertAccountingFlowsToSi(WaterSupplyAccounting accounting)
88+
throws DataSetIllegalArgumentException, IllegalArgumentException {
89+
String siFlowUnits = Parameter.getParameter(Parameter.PARAMID_FLOW)
90+
.getUnitsStringForSystem(UnitUtil.SI_ID);
91+
92+
Map<Instant, List<PumpTransfer>> converted = new java.util.TreeMap<>();
93+
for (Map.Entry<Instant, List<PumpTransfer>> entry : accounting.getPumpAccounting().entrySet()) {
94+
List<PumpTransfer> transformed = new java.util.ArrayList<>();
95+
for (PumpTransfer pt : entry.getValue()) {
96+
String fromUnits = pt.getFlowUnit();
97+
if (fromUnits != null && !fromUnits.equalsIgnoreCase(siFlowUnits)) {
98+
try {
99+
double siValue = UnitUtil.convertUnits(pt.getFlow(), fromUnits, siFlowUnits);
100+
transformed.add(new PumpTransfer(pt.getPumpType(), pt.getTransferTypeDisplay(), siValue,
101+
siFlowUnits, pt.getComment()));
102+
} catch (UnitsConversionException e) {
103+
throw new IllegalArgumentException(e.getMessage(), e);
104+
}
105+
} else {
106+
transformed.add(pt);
107+
}
108+
}
109+
converted.put(entry.getKey(), transformed);
110+
}
111+
return new WaterSupplyAccounting.Builder()
112+
.withWaterUser(accounting.getWaterUser())
113+
.withContractName(accounting.getContractName())
114+
.withPumpLocations(accounting.getPumpLocations())
115+
.withPumpAccounting(converted)
116+
.build();
117+
}
118+
72119
static WaterUserContract toWaterContract(WATER_USER_CONTRACT_OBJ_T contract) {
73120
Instant effectiveDate = null;
74121
if(contract.getWS_CONTRACT_EFFECTIVE_DATE() != null) {
@@ -251,7 +298,7 @@ static LOC_REF_TIME_WINDOW_TAB_T toTimeWindowTabT(WaterSupplyAccounting accounti
251298
}
252299

253300
static List<WaterSupplyAccounting> toWaterSupplyAccountingList(Connection c, WAT_USR_CONTRACT_ACCT_TAB_T
254-
watUsrContractAcctTabT) {
301+
watUsrContractAcctTabT, String flowUnits) {
255302

256303
List<WaterSupplyAccounting> waterSupplyAccounting = new ArrayList<>();
257304
Map<AccountingKey, WaterSupplyAccounting> cacheMap = new TreeMap<>();
@@ -269,9 +316,9 @@ static List<WaterSupplyAccounting> toWaterSupplyAccountingList(Connection c, WAT
269316
.build();
270317
if (cacheMap.containsKey(key)) {
271318
WaterSupplyAccounting accounting = cacheMap.get(key);
272-
addTransfer(watUsrContractAcctObjT, accounting);
319+
addTransfer(watUsrContractAcctObjT, accounting, flowUnits);
273320
} else {
274-
cacheMap.put(key, createAccounting(c, watUsrContractAcctObjT));
321+
cacheMap.put(key, createAccounting(c, watUsrContractAcctObjT, flowUnits));
275322
}
276323
}
277324
for (Map.Entry<AccountingKey, WaterSupplyAccounting> entry : cacheMap.entrySet()) {
@@ -280,7 +327,7 @@ static List<WaterSupplyAccounting> toWaterSupplyAccountingList(Connection c, WAT
280327
return waterSupplyAccounting;
281328
}
282329

283-
private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONTRACT_ACCT_OBJ_T acctObjT) {
330+
private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONTRACT_ACCT_OBJ_T acctObjT, String flowUnits) {
284331
WaterContractDao waterContractDao = new WaterContractDao(DSL.using(c));
285332
WATER_USER_OBJ_T waterUserObjT = acctObjT.getWATER_USER_CONTRACT_REF().getWATER_USER();
286333
WaterUserContract waterUserContract = waterContractDao.getWaterContract(
@@ -307,13 +354,13 @@ private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONT
307354
PumpTransfer transfer = null;
308355
if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(pumpLocation)
309356
&& pumpIn.getOfficeId().equalsIgnoreCase(pumpOffice)) {
310-
transfer = new PumpTransfer(PumpType.IN, transferDisplay, flow, remarks);
357+
transfer = new PumpTransfer(PumpType.IN, transferDisplay, flow, flowUnits, remarks);
311358
} else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(pumpLocation)
312359
&& pumpOut.getOfficeId().equalsIgnoreCase(pumpOffice)) {
313-
transfer = new PumpTransfer(PumpType.OUT, transferDisplay, flow, remarks);
360+
transfer = new PumpTransfer(PumpType.OUT, transferDisplay, flow, flowUnits, remarks);
314361
} else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(pumpLocation)
315362
&& pumpBelow.getOfficeId().equalsIgnoreCase(pumpOffice)) {
316-
transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, flow, remarks);
363+
transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, flow, flowUnits, remarks);
317364
}
318365
if (transfer != null) {
319366
pumpAccounting.put(transferStart, Collections.singletonList(transfer));
@@ -330,7 +377,7 @@ private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONT
330377
.build();
331378
}
332379

333-
private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupplyAccounting accounting) {
380+
private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupplyAccounting accounting, String flowUnits) {
334381
PumpTransfer transfer = null;
335382
String transferDisplay = acctObjTs.getPHYSICAL_TRANSFER_TYPE().getDISPLAY_VALUE();
336383
String accountingRemarks = acctObjTs.getACCOUNTING_REMARKS();
@@ -343,14 +390,14 @@ private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupp
343390

344391
if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(locationId)
345392
&& pumpIn.getOfficeId().equalsIgnoreCase(officeId)) {
346-
transfer = new PumpTransfer(PumpType.IN, transferDisplay, acctObjTs.getPUMP_FLOW(), accountingRemarks);
393+
transfer = new PumpTransfer(PumpType.IN, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks);
347394
} else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(locationId)
348395
&& pumpOut.getOfficeId().equalsIgnoreCase(officeId)) {
349-
transfer = new PumpTransfer(PumpType.OUT, transferDisplay, acctObjTs.getPUMP_FLOW(), accountingRemarks);
396+
transfer = new PumpTransfer(PumpType.OUT, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks);
350397
} else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(locationId)
351398
&& pumpBelow.getOfficeId().equalsIgnoreCase(officeId)) {
352399
transfer = new PumpTransfer(PumpType.BELOW, transferDisplay,
353-
acctObjTs.getPUMP_FLOW(), accountingRemarks);
400+
acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks);
354401
}
355402
if (accounting.getPumpAccounting().get(transferStart) != null) {
356403
List<PumpTransfer> transfers = new ArrayList<>(accounting.getPumpAccounting().get(transferStart));

cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/PumpTransfer.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
*
33
* MIT License
44
*
5-
* Copyright (c) 2024 Hydrologic Engineering Center
5+
* Copyright (c) 2026 Hydrologic Engineering Center
66
*
77
* Permission is hereby granted, free of charge, to any person obtaining a copy
88
* of this software and associated documentation files (the "Software"), to deal
@@ -41,14 +41,17 @@ public final class PumpTransfer extends CwmsDTOBase {
4141
@JsonProperty(required = true)
4242
private final Double flow;
4343
@JsonProperty(required = true)
44+
private final String flowUnit;
45+
@JsonProperty(required = true)
4446
private final String comment;
4547

4648
@JsonCreator
4749
public PumpTransfer(@JsonProperty("pump-type") PumpType pumpType,
48-
@JsonProperty("transfer-type-display") String transferTypeDisplay,
49-
@JsonProperty("flow") Double flow, @JsonProperty("comment") String comment) {
50+
@JsonProperty("transfer-type-display") String transferTypeDisplay,
51+
@JsonProperty("flow") Double flow, @JsonProperty("flow-unit") String flowUnit, @JsonProperty("comment") String comment) {
5052
this.transferTypeDisplay = transferTypeDisplay;
5153
this.flow = flow;
54+
this.flowUnit = flowUnit;
5255
this.comment = comment;
5356
this.pumpType = pumpType;
5457
}
@@ -61,6 +64,10 @@ public Double getFlow() {
6164
return this.flow;
6265
}
6366

67+
public String getFlowUnit() {
68+
return this.flowUnit;
69+
}
70+
6471
public PumpType getPumpType() {
6572
return this.pumpType;
6673
}

cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import static cwms.cda.data.dao.DaoTest.getDslContext;
6969
import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER;
7070
import static io.restassured.RestAssured.given;
71+
import static org.hamcrest.Matchers.closeTo;
7172
import static org.hamcrest.Matchers.equalTo;
7273
import static org.hamcrest.Matchers.is;
7374

@@ -449,6 +450,66 @@ void testStoreRetrieveWithUnits(String format) throws Exception {
449450
;
450451
}
451452

453+
@ParameterizedTest
454+
@ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT})
455+
void testStoreAsCfsThenRetrieveWithUnits(String format) throws Exception {
456+
TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL;
457+
String json = JsonV1.buildObjectMapper().writeValueAsString(waterSupplyAccounting);
458+
459+
json = json.replace("cms", "cfs");
460+
// create pump accounting
461+
given()
462+
.log().ifValidationFails(LogDetail.ALL, true)
463+
.contentType(Formats.JSONV1)
464+
.body(json)
465+
.header(AUTH_HEADER, user.toHeaderValue())
466+
.when()
467+
.redirects().follow(true)
468+
.redirects().max(3)
469+
.post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/"
470+
+ contract.getWaterUser().getEntityName() + "/contracts/"
471+
+ contract.getContractId().getName() + "/accounting")
472+
.then()
473+
.log().ifValidationFails(LogDetail.ALL, true)
474+
.assertThat()
475+
.statusCode(is(HttpServletResponse.SC_CREATED))
476+
;
477+
478+
// retrieve pump accounting
479+
given()
480+
.log().ifValidationFails(LogDetail.ALL, true)
481+
.contentType(Formats.JSONV1)
482+
.header(AUTH_HEADER, user.toHeaderValue())
483+
.accept(format)
484+
.queryParam(START_TIME, "2005-04-05T00:00:00Z")
485+
.queryParam(END_TIME, "2335-04-06T00:00:00Z")
486+
.queryParam(START_INCLUSIVE, "true")
487+
.queryParam(END_INCLUSIVE, "true")
488+
.queryParam(ASCENDING, "true")
489+
.queryParam(ROW_LIMIT, 100)
490+
.queryParam(UNIT, "cfs")
491+
.when()
492+
.redirects().follow(true)
493+
.redirects().max(3)
494+
.get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/"
495+
+ contract.getWaterUser().getEntityName() + "/contracts/"
496+
+ contract.getContractId().getName() + "/accounting")
497+
.then()
498+
.log().ifValidationFails(LogDetail.ALL, true)
499+
.assertThat()
500+
.statusCode(is(HttpServletResponse.SC_OK))
501+
.body("[0].contract-name", equalTo(waterSupplyAccounting.getContractName()))
502+
.body("[0].water-user.entity-name", equalTo(waterSupplyAccounting.getWaterUser().getEntityName()))
503+
.body("[0].water-user.project-id.name", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getName()))
504+
.body("[0].water-user.project-id.office-id", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getOfficeId()))
505+
.body("[0].water-user.water-right", equalTo(waterSupplyAccounting.getWaterUser().getWaterRight()))
506+
.body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].pump-type[2]", equalTo(String.format("%s", PumpType.IN)))
507+
.body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].flow[2].toDouble()", closeTo(1.0, 0.0001))
508+
.body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].transfer-type-display[2]", equalTo(testTransferType.getDisplayValue()))
509+
.body("[0].pump-locations.pump-in.name", equalTo(waterSupplyAccounting.getPumpLocations().getPumpIn().getName()))
510+
;
511+
}
512+
452513
@Test
453514
void testStoreNonExistentTransferType() throws Exception {
454515
TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL;

cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDaoIT.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -450,22 +450,22 @@ private WaterSupplyAccounting buildTestAccountingWithFewerPumps() {
450450
private Map<Instant, List<PumpTransfer>> buildTestPumpAccountingList() {
451451
Map<Instant, List<PumpTransfer>> retList = new TreeMap<>();
452452
List<PumpTransfer> transfers = new ArrayList<>();
453-
transfers.add(new PumpTransfer(PumpType.IN, "Conduit", 100.0, "Test Transfer"));
454-
transfers.add(new PumpTransfer(PumpType.OUT, "Pipeline", 200.0, "Emergency Transfer"));
453+
transfers.add(new PumpTransfer(PumpType.IN, "Conduit", 100.0, "cms", "Test Transfer"));
454+
transfers.add(new PumpTransfer(PumpType.OUT, "Pipeline", 200.0, "cms", "Emergency Transfer"));
455455
retList.put(Instant.parse("2025-10-01T00:00:00Z"), transfers);
456456
transfers.clear();
457-
transfers.add(new PumpTransfer(PumpType.OUT, "Canal", 300.0, "Test Transfer"));
458-
transfers.add(new PumpTransfer(PumpType.BELOW, "Stream", 400.0, "Emergency Transfer"));
457+
transfers.add(new PumpTransfer(PumpType.OUT, "Canal", 300.0, "cms", "Test Transfer"));
458+
transfers.add(new PumpTransfer(PumpType.BELOW, "Stream", 400.0, "cms", "Emergency Transfer"));
459459
retList.put(Instant.parse("2025-10-02T00:00:00Z"), transfers);
460460
return retList;
461461
}
462462

463463
private Map<Instant, List<PumpTransfer>> buildTestPumpAccountingListWithFewerPumps() {
464464
Map<Instant, List<PumpTransfer>> retList = new TreeMap<>();
465465
retList.put(Instant.parse("2025-10-01T00:00:00Z"),
466-
Collections.singletonList(new PumpTransfer(PumpType.IN, "Conduit", 560.0, "Test Transfer")));
466+
Collections.singletonList(new PumpTransfer(PumpType.IN, "Conduit", 560.0, "cms", "Test Transfer")));
467467
retList.put(Instant.parse("2025-10-02T00:00:00Z"),
468-
Collections.singletonList(new PumpTransfer(PumpType.IN, "Canal", 750.0, "Test Transfer")));
468+
Collections.singletonList(new PumpTransfer(PumpType.IN, "Canal", 750.0, "cms", "Test Transfer")));
469469
return retList;
470470
}
471471

0 commit comments

Comments
 (0)