Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/folio-org/mod-dcb into MO…
Browse files Browse the repository at this point in the history
…DDCB-81
  • Loading branch information
MagzhanArtykov committed Dec 12, 2023
2 parents c8e6629 + 98fe1c9 commit 9ace05c
Show file tree
Hide file tree
Showing 23 changed files with 540 additions and 79 deletions.
58 changes: 58 additions & 0 deletions PERSONAL_DATA_DISCLOSURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@


## Overview
The purpose of this form is to disclose the types of personal data stored by each module. This information enables those hosting FOLIO to better manage and comply with various privacy laws and restrictions, e.g. GDPR.

It's important to note that personal data is not limited to that which can be used to identify a person on it's own (e.g. Social security number), but also data used in conjunction with other data to identify a person (e.g. date of birth + city + gender).

For the purposes of this form, "store" includes the following:
* Persisting to storage - Either internal (e.g. Postgres) or external (e.g. S3, etc.) to FOLIO
* Caching - In-memory, etc.
* Logging
* Sending to an external piece of infrastructure such as a queue (e.g. Kafka), search engine (e.g. Elasticsearch), distributed table, etc.

## Personal Data Stored by This Module
- [ ] This module does not store any personal data.
- [ ] This module provides [custom fields](https://github.com/folio-org/folio-custom-fields).
- [ ] This module stores fields with free-form text (tags, notes, descriptions, etc.)
- [ ] This module caches personal data
---
- [ ] First name
- [ ] Last name
- [ ] Middle name
- [x] Pseudonym / Alias / Nickname / Username / User ID
- [ ] Gender
- [ ] Date of birth
- [ ] Place of birth
- [ ] Racial or ethnic origin
- [ ] Address
- [ ] Location information
- [ ] Phone numbers
- [ ] Passport number / National identification numbers
- [ ] Driver’s license number
- [ ] Social security number
- [ ] Email address
- [ ] Web cookies
- [ ] IP address
- [ ] Geolocation data
- [ ] Financial information
- [ ] Logic or algorithms used to build a user/profile
<!--- - [ ] Other personal data - Please list as needed -->
<!--- - [ ] Other personal data - Please list as needed -->

**NOTE** This is not intended to be a comprehensive list, but instead provide a starting point for module developers/maintainers to use.

## Privacy Laws, Regulations, and Policies
The following laws and policies were considered when creating the list of personal data fields above.
* [General Data Protection Regulation (GDPR)](https://gdpr.eu/)
* [California Consumer Privacy Act (CCPA)](https://oag.ca.gov/privacy/ccpa)
* [U.S. Department of Labor: Guidance on the Protection of Personal Identifiable Information](https://www.dol.gov/general/ppii)
* Cybersecurity Law of the People's Republic of China
* https://www.newamerica.org/cybersecurity-initiative/digichina/blog/translation-cybersecurity-law-peoples-republic-china/
* http://en.east-concord.com/zygd/Article/20203/ArticleContent_1690.html?utm_source=Mondaq&utm_medium=syndication&utm_campaign=LinkedIn-integration
* [Personal Data Protection Bill, 2019 (India)](https://www.prsindia.org/billtrack/personal-data-protection-bill-2019)
* [Data protection act 2018 (UK)](https://www.legislation.gov.uk/ukpga/2018/12/section/3/enacted)

---

v1.0
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package org.folio.dcb.client.feign;

import org.folio.dcb.domain.dto.CirculationItemRequest;
import org.folio.dcb.domain.dto.CirculationItem;
import org.folio.dcb.domain.dto.CirculationItemCollection;
import org.folio.spring.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "circulation-item", configuration = FeignClientConfiguration.class)
public interface CirculationItemClient {
@PostMapping("/{circulationItemId}")
CirculationItemRequest createCirculationItem(@PathVariable("circulationItemId") String circulationItemId, @RequestBody CirculationItemRequest circulationRequest);
CirculationItem createCirculationItem(@PathVariable("circulationItemId") String circulationItemId, @RequestBody CirculationItem circulationRequest);

@GetMapping("/{circulationItemId}")
CirculationItemRequest retrieveCirculationItemById(@PathVariable("circulationItemId") String circulationItemId);
CirculationItem retrieveCirculationItemById(@PathVariable("circulationItemId") String circulationItemId);

@PutMapping("/{circulationItemId}")
CirculationItemRequest updateCirculationItem(@PathVariable("circulationItemId") String circulationItemId, @RequestBody CirculationItemRequest circulationRequest);
}
@GetMapping("/items")
CirculationItemCollection fetchItemByIdAndBarcode(@RequestParam("query") String query);}
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,21 @@ public Errors handleGlobalException(Exception ex) {
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
public Errors handleNotFoundException(NotFoundException ex) {
@ExceptionHandler({
NotFoundException.class,
FeignException.NotFound.class
})
public Errors handleNotFoundException(Exception ex) {
logExceptionMessage(ex);
return createExternalError(ex.getMessage(), NOT_FOUND_ERROR);
}

@ResponseStatus(HttpStatus.CONFLICT)
@ExceptionHandler(ResourceAlreadyExistException.class)
public Errors handleAlreadyExistException(ResourceAlreadyExistException ex) {
@ExceptionHandler({
ResourceAlreadyExistException.class,
FeignException.Conflict.class
})
public Errors handleAlreadyExistException(Exception ex) {
logExceptionMessage(ex);
return createExternalError(ex.getMessage(), DUPLICATE_ERROR);
}
Expand All @@ -59,7 +65,8 @@ public Errors handleBadGatewayException(FeignException.BadGateway ex) {
MissingServletRequestParameterException.class,
MethodArgumentTypeMismatchException.class,
HttpMessageNotReadableException.class,
IllegalArgumentException.class
IllegalArgumentException.class,
FeignException.BadRequest.class
})
public Errors handleValidationErrors(Exception ex) {
logExceptionMessage(ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.folio.dcb.domain.dto.TransactionStatus;
import org.folio.dcb.rest.resource.TransactionsApi;
import org.folio.dcb.domain.dto.TransactionStatusResponse;
import org.folio.dcb.service.TransactionAuditService;
import org.folio.dcb.service.TransactionsService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -17,24 +18,49 @@
public class TransactionApiController implements TransactionsApi {

private final TransactionsService transactionsService;
private final TransactionAuditService transactionAuditService;
@Override
public ResponseEntity<TransactionStatusResponse> getTransactionStatusById(String dcbTransactionId) {
log.info("getTransactionStatus:: by id {} ", dcbTransactionId);
TransactionStatusResponse transactionStatusResponse;
try {
transactionStatusResponse = transactionsService.getTransactionStatusById(dcbTransactionId);
} catch (Exception ex) {
transactionAuditService.logErrorIfTransactionAuditExists(dcbTransactionId, ex.getMessage());
throw ex;
}

return ResponseEntity.status(HttpStatus.OK)
.body(transactionsService.getTransactionStatusById(dcbTransactionId));
.body(transactionStatusResponse);
}

@Override
public ResponseEntity<TransactionStatusResponse> createCirculationRequest(String dcbTransactionId, DcbTransaction dcbTransaction) {
log.info("createCirculationRequest:: creating dcbTransaction {} with id {} ", dcbTransaction, dcbTransactionId);
TransactionStatusResponse transactionStatusResponse;
try {
transactionStatusResponse = transactionsService.createCirculationRequest(dcbTransactionId, dcbTransaction);
} catch (Exception ex) {
transactionAuditService.logErrorIfTransactionAuditNotExists(dcbTransactionId, dcbTransaction, ex.getMessage());
throw ex;
}

return ResponseEntity.status(HttpStatus.CREATED)
.body(transactionsService.createCirculationRequest(dcbTransactionId, dcbTransaction));
.body(transactionStatusResponse);
}

@Override
public ResponseEntity<TransactionStatusResponse> updateTransactionStatus(String dcbTransactionId, TransactionStatus transactionStatus) {
log.info("updateTransactionStatus:: updating dcbTransaction with id {} to status {} ", dcbTransactionId, transactionStatus.getStatus());
TransactionStatusResponse transactionStatusResponse;
try {
transactionStatusResponse = transactionsService.updateTransactionStatus(dcbTransactionId, transactionStatus);
} catch (Exception ex) {
transactionAuditService.logErrorIfTransactionAuditExists(dcbTransactionId, ex.getMessage());
throw ex;
}

return ResponseEntity.status(HttpStatus.OK)
.body(transactionsService.updateTransactionStatus(dcbTransactionId, transactionStatus));
.body(transactionStatusResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class TransactionAuditEntity extends TransactionAuditableEntity {
@ColumnTransformer(write = "?::jsonb")
@Column(columnDefinition = "jsonb")
private String after;
private String errorMessage;

private String transactionId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -19,6 +20,7 @@
import org.folio.dcb.domain.dto.TransactionStatus.StatusEnum;
import org.folio.dcb.listener.entity.TransactionAuditEntityListener;

import java.io.Serializable;
import java.util.UUID;

@Entity
Expand All @@ -30,7 +32,7 @@
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TransactionEntity extends AuditableEntity {
public class TransactionEntity extends AuditableEntity implements Serializable {
@Id
private String id;
@Convert(converter = UUIDConverter.class)
Expand All @@ -51,6 +53,7 @@ public class TransactionEntity extends AuditableEntity {
private StatusEnum status;
@Enumerated(EnumType.STRING)
private DcbTransaction.RoleEnum role;

@Transient
protected TransactionEntity savedState;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,26 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.SerializationUtils;
import org.folio.dcb.domain.entity.TransactionAuditEntity;
import org.folio.dcb.domain.entity.TransactionEntity;
import org.folio.dcb.service.impl.TransactionsServiceImpl;
import org.folio.dcb.utils.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Component
@Log4j2
public class TransactionAuditEntityListener {
private static final String CREATE_ACTION = "CREATE";
private static final String UPDATE_ACTION = "UPDATE";

@Autowired
private BeanUtil beanUtil;
@Autowired
private ObjectMapper objectMapper;
private static final Map<String, TransactionEntity> originalStateCache = new HashMap<>();

@PrePersist
public void onPrePersist(Object entity) throws JsonProcessingException {
Expand All @@ -44,37 +38,25 @@ public void onPrePersist(Object entity) throws JsonProcessingException {
getEntityManager().persist(transactionAuditEntity);
}

@PostPersist
public void onPostPersist(Object entity) {
TransactionEntity transactionEntity = (TransactionEntity) entity;
originalStateCache.put(transactionEntity.getId(), transactionEntity);
}

@PreUpdate
public void onPreUpdate(Object entity) throws JsonProcessingException {
log.debug("onPreUpdate:: creating transaction audit record");
TransactionEntity transactionEntity = (TransactionEntity) entity;
TransactionAuditEntity transactionAuditEntity = new TransactionAuditEntity();
transactionAuditEntity.setBefore(objectMapper.writeValueAsString(getTransactionEntity(transactionEntity.getId())));
transactionAuditEntity.setBefore(objectMapper.writeValueAsString(transactionEntity.getSavedState()));
transactionAuditEntity.setAfter(objectMapper.writeValueAsString(transactionEntity));
transactionAuditEntity.setTransactionId(transactionEntity.getId());
transactionAuditEntity.setAction(UPDATE_ACTION);

log.info("onPreUpdate:: creating transaction audit record {} with action {}", transactionEntity.getId(), UPDATE_ACTION);
getEntityManager().persist(transactionAuditEntity);
originalStateCache.put(transactionEntity.getId(), transactionEntity);
}

private TransactionEntity getTransactionEntity(String transactionId) {
// Try to get the original state from the cache
TransactionEntity originalState = originalStateCache.get(transactionId);

// If not found, fetch it from the database
if (Objects.isNull(originalState)) {
originalState = getTransactionsService().getTransactionEntityOrThrow(transactionId);
originalStateCache.put(transactionId, originalState);
}
return originalState;
//This method will be invoked when the transactionEntity is loaded and the transactionEntity is stored in a transient field
//The stored value will be used in onPreUpdate method's setBefore method.
@PostLoad
public void saveState(TransactionEntity transactionEntity){
transactionEntity.setSavedState(SerializationUtils.clone((transactionEntity)));
}

//EntityListeners are instantiated by JPA, not Spring,
Expand All @@ -83,7 +65,4 @@ private EntityManager getEntityManager() {
return beanUtil.getBean(EntityManager.class);
}

private TransactionsServiceImpl getTransactionsService() {
return beanUtil.getBean(TransactionsServiceImpl.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.folio.dcb.repository;

import org.folio.dcb.domain.entity.TransactionAuditEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface TransactionAuditRepository extends JpaRepository<TransactionAuditEntity, String> {

@Query(value = "SELECT * FROM transactions_audit " +
"WHERE transaction_id = :trnId " +
"and created_date = (SELECT MAX(created_date) FROM transactions_audit WHERE transaction_id = :trnId);", nativeQuery = true)
Optional<TransactionAuditEntity> findLatestTransactionAuditEntityByDcbTransactionId(@Param("trnId") String trnId);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.folio.dcb.service;

import org.folio.dcb.domain.dto.CirculationItemRequest;
import org.folio.dcb.domain.dto.CirculationItem;
import org.folio.dcb.domain.dto.DcbItem;

public interface CirculationItemService {

void checkIfItemExistsAndCreate(DcbItem dcbTransaction, String pickupServicePointId);

CirculationItemRequest fetchItemById(String itemId);
CirculationItem fetchItemById(String itemId);

}
10 changes: 10 additions & 0 deletions src/main/java/org/folio/dcb/service/TransactionAuditService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.folio.dcb.service;

import org.folio.dcb.domain.dto.DcbTransaction;

public interface TransactionAuditService {

void logErrorIfTransactionAuditExists(String dcbTransactionId, String errorMsg);
void logErrorIfTransactionAuditNotExists(String dcbTransactionId, DcbTransaction dcbTransaction, String errorMsg);

}
12 changes: 6 additions & 6 deletions src/main/java/org/folio/dcb/service/impl/BaseLibraryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.ObjectUtils;
import org.folio.dcb.domain.dto.CirculationItemRequest;
import org.folio.dcb.domain.dto.CirculationItem;
import org.folio.dcb.domain.dto.CirculationRequest;
import org.folio.dcb.domain.dto.DcbTransaction;
import org.folio.dcb.domain.dto.TransactionStatus;
Expand Down Expand Up @@ -88,13 +88,13 @@ public void checkUserTypeAndThrowIfMismatch(String userType) {

public void updateStatusByTransactionEntity(TransactionEntity transactionEntity) {
log.debug("updateTransactionStatus:: Received checkIn event for itemId: {}", transactionEntity.getItemId());
CirculationItemRequest circulationItemRequest = circulationItemService.fetchItemById(transactionEntity.getItemId());
var circulationItemRequestStatus = circulationItemRequest.getStatus().getName();
if (OPEN == transactionEntity.getStatus() && AWAITING_PICKUP == circulationItemRequestStatus) {
CirculationItem circulationItem = circulationItemService.fetchItemById(transactionEntity.getItemId());
var circulationItemStatus = circulationItem.getStatus().getName();
if (OPEN == transactionEntity.getStatus() && AWAITING_PICKUP == circulationItemStatus) {
log.info(UPDATE_STATUS_BY_TRANSACTION_ENTITY_LOG_MESSAGE_PATTERN,
transactionEntity.getStatus().getValue(), TransactionStatus.StatusEnum.AWAITING_PICKUP.getValue());
updateTransactionEntity(transactionEntity, TransactionStatus.StatusEnum.AWAITING_PICKUP);
} else if (TransactionStatus.StatusEnum.AWAITING_PICKUP == transactionEntity.getStatus() && CHECKED_OUT == circulationItemRequestStatus) {
} else if (TransactionStatus.StatusEnum.AWAITING_PICKUP == transactionEntity.getStatus() && CHECKED_OUT == circulationItemStatus) {
log.info(UPDATE_STATUS_BY_TRANSACTION_ENTITY_LOG_MESSAGE_PATTERN,
transactionEntity.getStatus().getValue(), ITEM_CHECKED_OUT.getValue());
updateTransactionEntity(transactionEntity, ITEM_CHECKED_OUT);
Expand All @@ -104,7 +104,7 @@ public void updateStatusByTransactionEntity(TransactionEntity transactionEntity)
updateTransactionEntity(transactionEntity, TransactionStatus.StatusEnum.ITEM_CHECKED_IN);
} else {
log.info("updateStatusByTransactionEntity:: Item status is {}. So status of transaction is not updated",
circulationItemRequestStatus);
circulationItemStatus);
}

}
Expand Down
Loading

0 comments on commit 9ace05c

Please sign in to comment.