FINERACT-2490: Add status field to StandingInstructionData serialization (#5493)

This commit is contained in:
Ralph Hopman
2026-02-14 13:05:54 -06:00
committed by GitHub
parent 4bb5c02b19
commit 8a795e83c7
2 changed files with 186 additions and 0 deletions
@@ -63,6 +63,7 @@ public final class StandingInstructionData {
private final EnumOptionData transferType;
private final EnumOptionData priority;
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@Getter
private final BigDecimal amount;
@@ -0,0 +1,185 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.fineract.portfolio.account.data;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test class to verify JSON serialization of StandingInstructionData, particularly focusing on the status field
* visibility in API responses.
*/
public class StandingInstructionDataSerializationTest {
private ObjectMapper objectMapper;
@BeforeEach
public void setUp() {
objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules(); // Register JavaTimeModule for LocalDate support
}
/**
* This test verifies that the status field is properly serialized to JSON.
*
* BACKGROUND: The backend soft-delete implementation marks standing instructions as deleted (status=3). The
* database query correctly selects the status field, and the StandingInstructionMapper properly populates it.
* However, without the @Getter annotation on the private status field, Jackson cannot serialize it to JSON, making
* it impossible for frontend applications to: - Filter out deleted standing instructions (status=3) - Distinguish
* between ACTIVE (1), DISABLED (2), and DELETED (3) states
*
* EXPECTED BEHAVIOR: The status field should be included in JSON responses from GET /standinginstructions
*/
@Test
public void testStatusFieldSerialization() throws Exception {
// Create a status enum representing DELETED state (status=3)
EnumOptionData deletedStatus = new EnumOptionData(3L, "DELETED", "Deleted");
// Create a StandingInstructionData instance with required fields
StandingInstructionData standingInstruction = StandingInstructionData.instance(1L, // id
100L, // accountDetailId
"Test Standing Instruction", // name
null, // fromOffice
null, // toOffice
null, // fromClient
null, // toClient
null, // fromAccountType
null, // fromAccount
null, // toAccountType
null, // toAccount
null, // transferType
null, // priority
null, // instructionType
deletedStatus, // status (THIS IS THE CRITICAL FIELD)
BigDecimal.valueOf(1000), // amount
LocalDate.of(2026, 1, 1), // validFrom
LocalDate.of(2026, 12, 31), // validTill
null, // recurrenceType
null, // recurrenceFrequency
null, // recurrenceInterval
null // recurrenceOnMonthDay
);
// Serialize to JSON
String json = objectMapper.writeValueAsString(standingInstruction);
// Parse the JSON to verify field presence
JsonNode jsonNode = objectMapper.readTree(json);
// Verify basic fields are present
assertNotNull(jsonNode.get("id"), "id field should be present in JSON");
assertEquals(1L, jsonNode.get("id").asLong());
assertNotNull(jsonNode.get("name"), "name field should be present in JSON");
assertEquals("Test Standing Instruction", jsonNode.get("name").asText());
assertNotNull(jsonNode.get("amount"), "amount field should be present in JSON");
assertEquals(1000, jsonNode.get("amount").asInt());
assertNotNull(jsonNode.get("status"),
"status field MUST be present in JSON response for frontend to filter deleted standing instructions");
// Verify the status field contains the correct data
JsonNode statusNode = jsonNode.get("status");
assertNotNull(statusNode.get("id"), "status.id should be present");
assertEquals(3L, statusNode.get("id").asLong(), "status.id should be 3 for DELETED state");
assertNotNull(statusNode.get("code"), "status.code should be present");
assertEquals("DELETED", statusNode.get("code").asText());
assertNotNull(statusNode.get("value"), "status.value should be present");
assertEquals("Deleted", statusNode.get("value").asText());
}
/**
* Verify that the status field is properly serialized for different status values (ACTIVE, DISABLED, DELETED).
*/
@Test
public void testStatusFieldSerializationForDifferentStates() throws Exception {
// Test ACTIVE state (status=1)
EnumOptionData activeStatus = new EnumOptionData(1L, "ACTIVE", "Active");
StandingInstructionData activeInstruction = createStandingInstructionWithStatus(activeStatus);
String activeJson = objectMapper.writeValueAsString(activeInstruction);
JsonNode activeNode = objectMapper.readTree(activeJson);
assertNotNull(activeNode.get("status"), "status field should be present for ACTIVE state");
assertEquals(1L, activeNode.get("status").get("id").asLong());
assertEquals("ACTIVE", activeNode.get("status").get("code").asText());
// Test DISABLED state (status=2)
EnumOptionData disabledStatus = new EnumOptionData(2L, "DISABLED", "Disabled");
StandingInstructionData disabledInstruction = createStandingInstructionWithStatus(disabledStatus);
String disabledJson = objectMapper.writeValueAsString(disabledInstruction);
JsonNode disabledNode = objectMapper.readTree(disabledJson);
assertNotNull(disabledNode.get("status"), "status field should be present for DISABLED state");
assertEquals(2L, disabledNode.get("status").get("id").asLong());
assertEquals("DISABLED", disabledNode.get("status").get("code").asText());
// Test DELETED state (status=3)
EnumOptionData deletedStatus = new EnumOptionData(3L, "DELETED", "Deleted");
StandingInstructionData deletedInstruction = createStandingInstructionWithStatus(deletedStatus);
String deletedJson = objectMapper.writeValueAsString(deletedInstruction);
JsonNode deletedNode = objectMapper.readTree(deletedJson);
assertNotNull(deletedNode.get("status"), "status field should be present for DELETED state");
assertEquals(3L, deletedNode.get("status").get("id").asLong());
assertEquals("DELETED", deletedNode.get("status").get("code").asText());
}
/**
* Helper method to create a StandingInstructionData with a specific status.
*/
private StandingInstructionData createStandingInstructionWithStatus(EnumOptionData status) {
return StandingInstructionData.instance(1L, // id
100L, // accountDetailId
"Test Standing Instruction", // name
null, // fromOffice
null, // toOffice
null, // fromClient
null, // toClient
null, // fromAccountType
null, // fromAccount
null, // toAccountType
null, // toAccount
null, // transferType
null, // priority
null, // instructionType
status, // status
BigDecimal.valueOf(1000), // amount
LocalDate.of(2026, 1, 1), // validFrom
LocalDate.of(2026, 12, 31), // validTill
null, // recurrenceType
null, // recurrenceFrequency
null, // recurrenceInterval
null // recurrenceOnMonthDay
);
}
}