Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ Four token types are exposed via `POST /token` today. The underlying STS library

| API `type` | STS class / subclass | Description | REST API |
|---|---|---|:---:|
| `TOP_UP` | 0 / 0 | Transfer electricity credit (kWh) | **Yes** |
| `TOP_UP_KWH` | 0 / 0 | Transfer electricity credit (kWh) | **Yes** |
| `CLEAR_CREDIT` | 2 / 1 | Clear existing credit on meter | **Yes** |
| `CLEAR_TAMPER` | 2 / 5 | Clear tamper condition | **Yes** |
| `SET_POWER_LIMIT` | 2 / 0 | Set maximum power limit | **Yes** |

`TOP_UP` is still accepted as a **deprecated** wire alias for `TOP_UP_KWH` (same path) for older
callers. Prefer `TOP_UP_KWH` for new integrations.

Tokens are generated using the **Standard Transfer Algorithm (STA / EA07)** via the [Bouncy Castle](https://www.bouncycastle.org/) cryptographic library.

To add a new token type, see [Adding a token type](CONTRIBUTING.md#adding-a-token-type) in `CONTRIBUTING.md`.
Expand All @@ -37,7 +40,7 @@ POST /token ──► TokenController
▼ dispatches to matching strategy
TokenStrategy (interface)
│
├── TransferElectricityCreditStrategy (TOP_UP)
├── TransferElectricityCreditStrategy (TOP_UP_KWH)
├── ClearCreditStrategy (CLEAR_CREDIT)
├── ClearTamperStrategy (CLEAR_TAMPER)
└── SetMaximumPowerLimitStrategy (SET_POWER_LIMIT)
Expand Down Expand Up @@ -259,11 +262,11 @@ Generates a prepayment token.

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | `string` | Yes | Token type: `TOP_UP`, `CLEAR_CREDIT`, `CLEAR_TAMPER`, `SET_POWER_LIMIT` |
| `type` | `string` | Yes | Token type: `TOP_UP_KWH`, `CLEAR_CREDIT`, `CLEAR_TAMPER`, `SET_POWER_LIMIT` (deprecated alias: `TOP_UP` → `TOP_UP_KWH`) |
| `issueDate` | `string` | Yes | ISO 8601 datetime (see note below) |
| `randomNumber` | `integer` | Yes | STS 4-bit RND field — **must be 0–15** (see note below) |
| `decoderKey` | `string` | Yes | Meter decoder key as a hexadecimal string (16 hex chars = 8 bytes) |
| `kwh` | `number` | For `TOP_UP` | Amount of electricity credit in kWh |
| `kwh` | `number` | For `TOP_UP_KWH` | Amount of electricity credit in kWh (also required when using deprecated `TOP_UP`) |
| `powerLimit` | `integer` | For `SET_POWER_LIMIT` | Maximum power limit value |

> **`randomNumber` — STS protocol constraint**
Expand All @@ -285,13 +288,13 @@ Generates a prepayment token.
> are allowed. Any time-zone offset is **ignored**; the date and time fields are
> interpreted as **UTC** for TID calculation, independent of the server's timezone.

**Example — TOP_UP**
**Example — TOP_UP_KWH**

```bash
curl -X POST http://localhost:8080/token \
-H "Content-Type: application/json" \
-d '{
"type": "TOP_UP",
"type": "TOP_UP_KWH",
"issueDate": "2024-03-15T10:30:00",
"randomNumber": 3,
"decoderKey": "XXXXXXXXXXXXXXXX",
Expand Down
6 changes: 5 additions & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ This document maps **IEC 62055-41 (STS)** token types to what exists in the code

All generators on the live path use the **Standard Transfer Algorithm (STA / EA07)** with a caller-supplied decoder key.

**Deprecated wire alias:** `TOP_UP` is still accepted on `POST /token` and is normalized to
`TOP_UP_KWH` at deserialize time (same strategy / generator path). Prefer `TOP_UP_KWH` for new
callers; remove `TOP_UP` once older servers have been updated.

---

## Token generation and decode

| API `type` | STS class / subclass | Description | Generate | Decode | REST API |
|---|---|---|:---:|:---:|:---:|
| `TOP_UP` | 0 / 0 | Transfer electricity credit (kWh) | Yes | — | **Yes** |
| `TOP_UP_KWH` | 0 / 0 | Transfer electricity credit (kWh) | Yes | — | **Yes** |
| — | 0 / 1 | Transfer water credit | Yes | — | No |
| — | 0 / 2 | Transfer gas credit | Yes | — | No |
| — | 0 / 3 | Time token | No | No | No |
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>co.nxtgrid</groupId>
<artifactId>nxt-sts</artifactId>
<version>1.0.1</version>
<version>1.0.2</version>

<parent>
<groupId>org.springframework.boot</groupId>
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/co/nxtgrid/api/StsExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/co/nxtgrid/api/TokenRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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"
)
Expand All @@ -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")
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/co/nxtgrid/api/TokenType.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package co.nxtgrid.api;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

/**
* REST {@code type} values for {@code POST /token}.
*
* <p>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. */
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/co/nxtgrid/api/TokenTypeDeserializer.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Accepts deprecated wire value {@code TOP_UP} and maps it to
* {@link TokenType#TOP_UP_KWH} immediately.
*/
public class TokenTypeDeserializer extends JsonDeserializer<TokenType> {

@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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 29 additions & 5 deletions src/test/java/co/nxtgrid/TokenControllerValidationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;

Expand Down Expand Up @@ -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"
)
);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
22 changes: 20 additions & 2 deletions src/test/java/co/nxtgrid/TokenStrategyIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
{
Expand All @@ -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
Expand Down
Loading