Skip to content

Commit

Permalink
feat(Transaction): implement sensorChaincodeTransaction API with opti…
Browse files Browse the repository at this point in the history
…onal filters
  • Loading branch information
TonyWu3027 committed Aug 24, 2023
1 parent 1d35915 commit 4d377b6
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovedTempReading;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.SensorChaincodeTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.Transaction;

@RestController
Expand Down Expand Up @@ -39,4 +40,14 @@ public List<Transaction> getTransactions(@RequestParam(required = false) Integer
return transactionService.getTransactions(containerNum);
}

@GetMapping("/sensorChaincodeTransactions")
public List<SensorChaincodeTransaction> getSensorChaincodeTransactions(
@RequestParam(required = false) Integer containerNum,
@RequestParam(required = false) Long sinceTimestamp,
@RequestParam(required = false) Long untilTimestamp,
@RequestParam(required = false) Long approvalWindowSeconds) {
return transactionService.getSensorChaincodeTransactions(containerNum, sinceTimestamp,
untilTimestamp, approvalWindowSeconds);
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package uk.ac.ic.doc.blocc.dashboard.transaction;

import jakarta.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovalTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovedTempReading;
Expand All @@ -16,6 +19,7 @@
import uk.ac.ic.doc.blocc.dashboard.transaction.repository.ApprovalTransactionRepository;
import uk.ac.ic.doc.blocc.dashboard.transaction.repository.SensorChaincodeTransactionRepository;
import uk.ac.ic.doc.blocc.dashboard.transaction.repository.TransactionRepository;
import uk.ac.ic.doc.blocc.dashboard.transaction.specification.SensorChaincodeTransactionSpecification;

@Service
public class TransactionService {
Expand Down Expand Up @@ -118,4 +122,39 @@ private static List<ApprovedTempReading> convertToApprovedTempReadings(
tx.getApprovalCount(),
tx.getTxId())).toList();
}

public List<SensorChaincodeTransaction> getSensorChaincodeTransactions(Integer containerNum,
Long sinceTimestamp,
Long untilTimestamp, Long approvalWindowSeconds) {
Specification<SensorChaincodeTransaction> specification =
SensorChaincodeTransactionSpecification.filterByParameters(
containerNum, sinceTimestamp,
untilTimestamp);

List<SensorChaincodeTransaction> filteredSensorChaincodeTransactions = sensorChaincodeTransactionRepository.findAll(
specification);

if (approvalWindowSeconds == null) {
return filteredSensorChaincodeTransactions;
}

List<SensorChaincodeTransaction> result = new ArrayList<>();

for (SensorChaincodeTransaction transaction : filteredSensorChaincodeTransactions) {
List<ApprovalTransaction> validApprovals = transaction.getApprovals().stream()
.filter(approval -> isWithinWindow(transaction.getCreatedTimestamp(),
approval.getCreatedTimestamp(), approvalWindowSeconds))
.collect(Collectors.toList());

transaction.setApprovals(validApprovals);
result.add(transaction);
}

return result;
}


private boolean isWithinWindow(Long transactionTimestamp, Long approvalTimestamp, Long window) {
return (approvalTimestamp - transactionTimestamp) <= window;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,12 @@ public int getApprovalCount() {
return approvals.size();
}

@JsonIgnore
public List<ApprovalTransaction> getApprovals() {
return approvals;
}

public void setApprovals(List<ApprovalTransaction> approvals) {
this.approvals = approvals;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.SensorChaincodeTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.CompositeKey;

@Repository
public interface SensorChaincodeTransactionRepository
extends JpaRepository<SensorChaincodeTransaction, CompositeKey> {
extends JpaRepository<SensorChaincodeTransaction, CompositeKey>,
JpaSpecificationExecutor<SensorChaincodeTransaction> {

@Query("SELECT tx FROM SensorChaincodeTransaction tx WHERE tx.key.containerNum = ?1 ORDER BY tx.reading.timestamp")
List<SensorChaincodeTransaction> findAllByContainerNum(int containerNum);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package uk.ac.ic.doc.blocc.dashboard.transaction.specification;

import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.SensorChaincodeTransaction;

public class SensorChaincodeTransactionSpecification {

public static Specification<SensorChaincodeTransaction> filterByParameters(Integer containerNum,
Long sinceTimestamp, Long untilTimestamp) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();

if (containerNum != null) {
predicates.add(criteriaBuilder.equal(root.get("key").get("containerNum"), containerNum));
}
if (sinceTimestamp != null) {
predicates.add(
criteriaBuilder.greaterThanOrEqualTo(root.get("createdTimestamp"), sinceTimestamp));
}
if (untilTimestamp != null) {
predicates.add(
criteriaBuilder.lessThanOrEqualTo(root.get("createdTimestamp"), untilTimestamp));
}

query.orderBy(criteriaBuilder.asc(root.get("createdTimestamp")));

return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,83 @@ public void getApprovedTransactionsSinceSpecificTimestamp() throws Exception {
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(2));
}

// Inside TransactionControllerTest class

@Test
public void getSensorChaincodeTransactionsWithoutParameters() throws Exception {
List<SensorChaincodeTransaction> mockData = Arrays.asList(
new SensorChaincodeTransaction("tx123", 1, "Container1MSP", 100L,
new TemperatureHumidityReading(25, 0.3F, 105L)),
new SensorChaincodeTransaction("tx456", 2, "Container2MSP", 110L,
new TemperatureHumidityReading(26, 0.4F, 115L))
);

when(transactionService.getSensorChaincodeTransactions(null, null, null, null)).thenReturn(
mockData);

mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/transaction/sensorChaincodeTransactions"))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(mockData.size()));
}

@Test
public void getSensorChaincodeTransactionsWithContainerNum() throws Exception {
int containerNum = 1;
List<SensorChaincodeTransaction> mockData = List.of(
new SensorChaincodeTransaction("tx123", 1, "Container1MSP", 100L,
new TemperatureHumidityReading(25, 0.3F, 105L))
);

when(transactionService.getSensorChaincodeTransactions(containerNum, null, null,
null)).thenReturn(mockData);

mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/transaction/sensorChaincodeTransactions")
.param("containerNum", String.valueOf(containerNum)))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(mockData.size()));
}

@Test
public void getSensorChaincodeTransactionsWithContainerNumAndSinceTimestamp() throws Exception {
int containerNum = 1;
long sinceTimestamp = 100L;
List<SensorChaincodeTransaction> mockData = List.of(
new SensorChaincodeTransaction("tx123", 1, "Container1MSP", 100L,
new TemperatureHumidityReading(25, 0.3F, 105L))
);

when(transactionService.getSensorChaincodeTransactions(containerNum, sinceTimestamp, null,
null)).thenReturn(mockData);

mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/transaction/sensorChaincodeTransactions")
.param("containerNum", String.valueOf(containerNum))
.param("sinceTimestamp", String.valueOf(sinceTimestamp)))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(mockData.size()));
}

@Test
public void getSensorChaincodeTransactionsWithAllParameters() throws Exception {
int containerNum = 1;
long sinceTimestamp = 90L;
long untilTimestamp = 120L;
long approvalWindowSeconds = 50L;
List<SensorChaincodeTransaction> mockData = List.of(
new SensorChaincodeTransaction("tx123", 1, "Container1MSP", 100L,
new TemperatureHumidityReading(25, 0.3F, 105L))
);

when(transactionService.getSensorChaincodeTransactions(containerNum, sinceTimestamp,
untilTimestamp, approvalWindowSeconds)).thenReturn(mockData);

mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/transaction/sensorChaincodeTransactions")
.param("containerNum", String.valueOf(containerNum))
.param("sinceTimestamp", String.valueOf(sinceTimestamp))
.param("untilTimestamp", String.valueOf(untilTimestamp))
.param("approvalWindowSeconds", String.valueOf(approvalWindowSeconds)))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(mockData.size()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -16,6 +17,7 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.data.jpa.domain.Specification;
import uk.ac.ic.doc.blocc.dashboard.fabric.model.TemperatureHumidityReading;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovalTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovedTempReading;
Expand Down Expand Up @@ -308,5 +310,66 @@ public void getsApprovedTempReadingsSinceTimestamp() {
assertTrue(result.containsAll(expectedReadings));
}

@Test
public void getsSensorChaincodeTransactionsFiltersApprovalsByWindow() {
// Given
SensorChaincodeTransaction tx1 = new SensorChaincodeTransaction(
"tx123", 1, "Container5MSP", 100L, new TemperatureHumidityReading(25, 0.3F, 105L));
SensorChaincodeTransaction tx2 = new SensorChaincodeTransaction(
"tx456", 1, "Container5MSP", 103L, new TemperatureHumidityReading(30, 0.2F, 110L));

ApprovalTransaction approval1 = new ApprovalTransaction("app1", 1, "Container6MSP", 102L, tx1);
ApprovalTransaction approval2 = new ApprovalTransaction("app2", 1, "Container7MSP", 108L,
tx1); // this will be filtered out
tx1.setApprovals(Arrays.asList(approval1, approval2));

when(sensorChaincodeTransactionRepository.findAll(anySensorTransactionSpecification()))
.thenReturn(Arrays.asList(tx1, tx2));

// When
List<SensorChaincodeTransaction> transactions = transactionService.getSensorChaincodeTransactions(
1, 100L, 110L, 3L);

// Then
assertEquals(2, transactions.size());
SensorChaincodeTransaction fetchedTx1 = transactions.get(0);
assertEquals(1, fetchedTx1.getApprovals().size());
assertEquals(approval1, fetchedTx1.getApprovals().get(0));
}

@Test
public void getsSensorChaincodeTransactionsFiltersApprovalsWithoutWindow() {
SensorChaincodeTransaction tx1 = new SensorChaincodeTransaction(
"tx123", 1, "Container5MSP", 100L, null
);

ApprovalTransaction approval1 = new ApprovalTransaction(
"app1", 1, "Container6MSP", 101L, tx1
);

ApprovalTransaction approval2 = new ApprovalTransaction(
"app2", 1, "Container7MSP", 105L, tx1
);

tx1.setApprovals(Arrays.asList(approval1, approval2));

when(sensorChaincodeTransactionRepository.findAll(anySensorTransactionSpecification()))
.thenReturn(List.of(tx1));

// The actual call
List<SensorChaincodeTransaction> result = transactionService.getSensorChaincodeTransactions(1,
null, null, null);

// Verifying that the tx1 was returned with both its approvals
assertEquals(1, result.size());
assertEquals(tx1.getTxId(), result.get(0).getTxId());
assertEquals(2, result.get(0).getApprovals().size());
}


private Specification<SensorChaincodeTransaction> anySensorTransactionSpecification() {
return any();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import uk.ac.ic.doc.blocc.dashboard.fabric.model.TemperatureHumidityReading;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.ApprovalTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.model.SensorChaincodeTransaction;
import uk.ac.ic.doc.blocc.dashboard.transaction.specification.SensorChaincodeTransactionSpecification;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
Expand Down Expand Up @@ -152,5 +153,43 @@ public void findAllSinceTimestampByContainerNumReturnsInOrderOfReadingTimestamp(
assertThat(found).hasSize(3).containsExactly(tx5, tx4, tx2);
}

@Test
public void findAllFilteredByParametersOrderedByCreatedTimestamp() {
SensorChaincodeTransaction tx1 = new SensorChaincodeTransaction(
"tx123",
1,
"Container5MSP",
95L,
new TemperatureHumidityReading(28, 0.25F, 102L)
);

SensorChaincodeTransaction tx2 = new SensorChaincodeTransaction(
"tx456",
1,
"Container5MSP",
100L,
new TemperatureHumidityReading(28, 0.25F, 102L)
);

SensorChaincodeTransaction tx3 = new SensorChaincodeTransaction(
"tx789",
1,
"Container5MSP",
105L,
new TemperatureHumidityReading(28, 0.25F, 102L)
);

entityManager.persist(tx1);
entityManager.persist(tx2);
entityManager.persist(tx3);
entityManager.flush();

List<SensorChaincodeTransaction> transactions = repository.findAll(
SensorChaincodeTransactionSpecification.filterByParameters(1, null, null)
);

assertThat(transactions).hasSize(3);
// Check the order based on createdTimestamp
assertThat(transactions).containsExactly(tx1, tx2, tx3);
}
}

0 comments on commit 4d377b6

Please sign in to comment.