org.springframework.boot
diff --git a/src/main/java/co/nxtgrid/api/StsExceptionHandler.java b/src/main/java/co/nxtgrid/api/StsExceptionHandler.java
index e2b1cb3..855a4c8 100644
--- a/src/main/java/co/nxtgrid/api/StsExceptionHandler.java
+++ b/src/main/java/co/nxtgrid/api/StsExceptionHandler.java
@@ -78,7 +78,8 @@ private static String messageForMalformedJson(HttpMessageNotReadableException ex
Throwable cause = ex.getCause();
if (cause instanceof InvalidFormatException invalidFormat) {
if (invalidFormat.getTargetType() == TokenType.class) {
- return "type must be one of: TOP_UP, CLEAR_CREDIT, CLEAR_TAMPER, SET_POWER_LIMIT";
+ return "type must be one of: TOP_UP_KWH, TOP_UP (deprecated alias), "
+ + "CLEAR_CREDIT, CLEAR_TAMPER, SET_POWER_LIMIT";
}
if (invalidFormat.getTargetType() == LocalDateTime.class || isField(invalidFormat, "issueDate")) {
return ISSUE_DATE_FORMAT_MESSAGE;
diff --git a/src/main/java/co/nxtgrid/api/TokenRequest.java b/src/main/java/co/nxtgrid/api/TokenRequest.java
index 7a9d73d..ac55a9a 100644
--- a/src/main/java/co/nxtgrid/api/TokenRequest.java
+++ b/src/main/java/co/nxtgrid/api/TokenRequest.java
@@ -25,7 +25,12 @@ public class TokenRequest {
@Pattern(regexp = "^[0-9A-Fa-f]{16}$", message = "decoderKey must be exactly 16 hex characters")
private String decoderKey;
- @Schema(description = "STS token type to generate", example = "TOP_UP")
+ @Schema(
+ description = "STS token type to generate. Prefer TOP_UP_KWH; TOP_UP is a deprecated "
+ + "alias for the same electricity kWh credit token.",
+ example = "TOP_UP_KWH",
+ allowableValues = { "TOP_UP_KWH", "TOP_UP", "CLEAR_CREDIT", "CLEAR_TAMPER", "SET_POWER_LIMIT" }
+ )
@NotNull
private TokenType type;
@@ -55,7 +60,8 @@ public class TokenRequest {
private Integer randomNumber;
@Schema(
- description = "Amount of electricity credit in kWh. Required when type is TOP_UP. Must be zero or greater.",
+ description = "Amount of electricity credit in kWh. Required when type is TOP_UP_KWH "
+ + "(or deprecated alias TOP_UP). Must be zero or greater.",
example = "0.5",
minimum = "0"
)
@@ -70,10 +76,10 @@ public class TokenRequest {
@PositiveOrZero(message = "powerLimit must be zero or greater")
private Long powerLimit;
- @AssertTrue(message = "kwh is required for TOP_UP")
+ @AssertTrue(message = "kwh is required for TOP_UP_KWH")
@JsonIgnore
public boolean isKwhValidForType() {
- return type != TokenType.TOP_UP || kwh != null;
+ return type != TokenType.TOP_UP_KWH || kwh != null;
}
@AssertTrue(message = "powerLimit is required for SET_POWER_LIMIT")
diff --git a/src/main/java/co/nxtgrid/api/TokenType.java b/src/main/java/co/nxtgrid/api/TokenType.java
index 07ddf6c..04f2e85 100644
--- a/src/main/java/co/nxtgrid/api/TokenType.java
+++ b/src/main/java/co/nxtgrid/api/TokenType.java
@@ -1,8 +1,18 @@
package co.nxtgrid.api;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * REST {@code type} values for {@code POST /token}.
+ *
+ * Electricity kWh credit is {@link #TOP_UP_KWH}. The wire value {@code TOP_UP} is still
+ * accepted for older callers and is normalized to {@link #TOP_UP_KWH} at deserialize time
+ * (see {@link TokenTypeDeserializer}).
+ */
+@JsonDeserialize(using = TokenTypeDeserializer.class)
public enum TokenType {
- /** Class 0 — transfers electricity credit to the meter. */
- TOP_UP,
+ /** Class 0 / subclass 0 — transfer electricity credit in kWh. */
+ TOP_UP_KWH,
/** Class 2 — clears existing credit balance on the meter. */
CLEAR_CREDIT,
/** Class 2 — clears the tamper condition flag on the meter. */
diff --git a/src/main/java/co/nxtgrid/api/TokenTypeDeserializer.java b/src/main/java/co/nxtgrid/api/TokenTypeDeserializer.java
new file mode 100644
index 0000000..0d9da95
--- /dev/null
+++ b/src/main/java/co/nxtgrid/api/TokenTypeDeserializer.java
@@ -0,0 +1,33 @@
+package co.nxtgrid.api;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+
+/**
+ * Deserializes {@link TokenType} from the JSON {@code type} string.
+ *
+ *
Accepts deprecated wire value {@code TOP_UP} and maps it to
+ * {@link TokenType#TOP_UP_KWH} immediately.
+ */
+public class TokenTypeDeserializer extends JsonDeserializer {
+
+ @Override
+ public TokenType deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+ String value = parser.getValueAsString();
+ if (value == null) {
+ return null;
+ }
+ if ("TOP_UP".equals(value) || "TOP_UP_KWH".equals(value)) {
+ return TokenType.TOP_UP_KWH;
+ }
+ try {
+ return TokenType.valueOf(value);
+ } catch (IllegalArgumentException ex) {
+ throw InvalidFormatException.from(parser, "Invalid TokenType value", value, TokenType.class);
+ }
+ }
+}
diff --git a/src/main/java/co/nxtgrid/strategy/TransferElectricityCreditStrategy.java b/src/main/java/co/nxtgrid/strategy/TransferElectricityCreditStrategy.java
index 127f760..7cf3514 100644
--- a/src/main/java/co/nxtgrid/strategy/TransferElectricityCreditStrategy.java
+++ b/src/main/java/co/nxtgrid/strategy/TransferElectricityCreditStrategy.java
@@ -25,7 +25,7 @@ public class TransferElectricityCreditStrategy implements TokenStrategy {
@Override
public boolean supports(TokenType type) {
- return TokenType.TOP_UP == type;
+ return TokenType.TOP_UP_KWH == type;
}
@Override
diff --git a/src/test/java/co/nxtgrid/TokenControllerValidationTest.java b/src/test/java/co/nxtgrid/TokenControllerValidationTest.java
index f0309c3..da436dc 100644
--- a/src/test/java/co/nxtgrid/TokenControllerValidationTest.java
+++ b/src/test/java/co/nxtgrid/TokenControllerValidationTest.java
@@ -15,7 +15,8 @@
@AutoConfigureMockMvc
class TokenControllerValidationTest {
- private static final String VALID_TOP_UP = """
+ /** Deprecated wire alias — still accepted for older callers. */
+ private static final String VALID_TOP_UP_ALIAS = """
{
"type": "TOP_UP",
"issueDate": "2024-03-15T10:30:00",
@@ -25,6 +26,16 @@ class TokenControllerValidationTest {
}
""";
+ private static final String VALID_TOP_UP_KWH = """
+ {
+ "type": "TOP_UP_KWH",
+ "issueDate": "2024-03-15T10:30:00",
+ "randomNumber": 3,
+ "decoderKey": "0123456789ABCDEF",
+ "kwh": 0.5
+ }
+ """;
+
@Autowired
private MockMvc mockMvc;
@@ -70,7 +81,10 @@ void rejectsUnknownTokenType() throws Exception {
.andExpect(status().isBadRequest())
.andExpect(
jsonPath("$.error")
- .value("type must be one of: TOP_UP, CLEAR_CREDIT, CLEAR_TAMPER, SET_POWER_LIMIT")
+ .value(
+ "type must be one of: TOP_UP_KWH, TOP_UP (deprecated alias), "
+ + "CLEAR_CREDIT, CLEAR_TAMPER, SET_POWER_LIMIT"
+ )
);
}
@@ -155,7 +169,7 @@ void rejectsMissingKwhForTopUp() throws Exception {
)
)
.andExpect(status().isBadRequest())
- .andExpect(jsonPath("$.error").value("kwh is required for TOP_UP"));
+ .andExpect(jsonPath("$.error").value("kwh is required for TOP_UP_KWH"));
}
@Test
@@ -274,9 +288,19 @@ void rejectsMalformedIssueDate() throws Exception {
}
@Test
- void acceptsValidRequest() throws Exception {
+ void acceptsTopUpKwh() throws Exception {
+ mockMvc.perform(
+ post("/token").contentType(MediaType.APPLICATION_JSON).content(VALID_TOP_UP_KWH)
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.token").isString())
+ .andExpect(jsonPath("$.token").isNotEmpty());
+ }
+
+ @Test
+ void acceptsDeprecatedTopUpAlias() throws Exception {
mockMvc.perform(
- post("/token").contentType(MediaType.APPLICATION_JSON).content(VALID_TOP_UP)
+ post("/token").contentType(MediaType.APPLICATION_JSON).content(VALID_TOP_UP_ALIAS)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").isString())
diff --git a/src/test/java/co/nxtgrid/TokenStrategyIntegrationTest.java b/src/test/java/co/nxtgrid/TokenStrategyIntegrationTest.java
index 01921a0..41d66e0 100644
--- a/src/test/java/co/nxtgrid/TokenStrategyIntegrationTest.java
+++ b/src/test/java/co/nxtgrid/TokenStrategyIntegrationTest.java
@@ -17,12 +17,30 @@ class TokenStrategyIntegrationTest {
private static final String DECODER_KEY = "0123456789ABCDEF";
private static final String ISSUE_DATE = "2024-03-15T10:30:00";
+ private static final String EXPECTED_TOP_UP_KWH_TOKEN = "58627975513348563046";
@Autowired
private MockMvc mockMvc;
@Test
- void topUp_producesExpectedToken() throws Exception {
+ void topUpKwh_producesExpectedToken() throws Exception {
+ postToken(
+ """
+ {
+ "type": "TOP_UP_KWH",
+ "issueDate": "%s",
+ "randomNumber": 3,
+ "decoderKey": "%s",
+ "kwh": 0.5
+ }
+ """.formatted(ISSUE_DATE, DECODER_KEY)
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.token").value(EXPECTED_TOP_UP_KWH_TOKEN));
+ }
+
+ @Test
+ void topUpDeprecatedAlias_producesSameTokenAsTopUpKwh() throws Exception {
postToken(
"""
{
@@ -35,7 +53,7 @@ void topUp_producesExpectedToken() throws Exception {
""".formatted(ISSUE_DATE, DECODER_KEY)
)
.andExpect(status().isOk())
- .andExpect(jsonPath("$.token").value("58627975513348563046"));
+ .andExpect(jsonPath("$.token").value(EXPECTED_TOP_UP_KWH_TOKEN));
}
@Test