Skip to content

Commit d782daa

Browse files
author
David Grace
authored
Merge pull request #3 from gracemann365/feature/shared-lib
Phase 2: Shared-Libs & API Infra — DTO, Exception, Validation, Idempotency, Logging, HealthFeature/shared lib
2 parents 07a7be7 + 64aa4ab commit d782daa

File tree

19 files changed

+354
-47
lines changed

19 files changed

+354
-47
lines changed

api-service/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,23 @@
3131
<artifactId>postgresql</artifactId>
3232
<scope>runtime</scope>
3333
</dependency>
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter-test</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
3440

3541
<dependency>
3642
<groupId>org.flywaydb</groupId>
3743
<artifactId>flyway-core</artifactId>
3844
</dependency>
45+
46+
<dependency>
47+
<groupId>com.openpay</groupId>
48+
<artifactId>shared-libs</artifactId>
49+
<version>1.0-SNAPSHOT</version>
50+
</dependency>
3951
</dependencies>
4052
<build>
4153
<plugins>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.openpay.api.config;
2+
3+
import java.io.IOException;
4+
import java.util.UUID;
5+
6+
import org.slf4j.MDC;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.filter.OncePerRequestFilter;
9+
10+
import jakarta.servlet.FilterChain;
11+
import jakarta.servlet.ServletException;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
15+
@Component
16+
public class LoggingFilter extends OncePerRequestFilter {
17+
18+
@Override
19+
protected void doFilterInternal(
20+
HttpServletRequest request,
21+
HttpServletResponse response,
22+
FilterChain filterChain) throws ServletException, IOException {
23+
24+
// Generate a unique request ID (can also use header if present)
25+
String requestId = UUID.randomUUID().toString();
26+
27+
// Put it into MDC
28+
MDC.put("requestId", requestId);
29+
30+
try {
31+
filterChain.doFilter(request, response);
32+
} finally {
33+
// Always clear MDC after the request to prevent memory leaks
34+
MDC.clear();
35+
}
36+
}
37+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.openpay.api.controller;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RestController;
5+
6+
@RestController
7+
public class HealthController {
8+
@GetMapping("/health")
9+
public String health() {
10+
return "UP";
11+
}
12+
}

api-service/src/main/java/com/openpay/api/controller/StatusController.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import com.openpay.api.model.TransactionEntity;
1212
import com.openpay.api.repository.TransactionRepository;
13+
import com.openpay.shared.dto.StatusResponse;
1314

1415
@RestController
1516
@RequestMapping("/transaction")
@@ -29,25 +30,9 @@ public ResponseEntity<?> getStatus(@PathVariable("id") Long id) {
2930
return ResponseEntity.notFound().build();
3031
}
3132

33+
// *** NO CHANGE: just use the shared StatusResponse ***
3234
return ResponseEntity.ok(
3335
new StatusResponse(id, transaction.get().getStatus()));
3436
}
35-
36-
static class StatusResponse {
37-
private Long id;
38-
private String status;
39-
40-
public StatusResponse(Long id, String status) {
41-
this.id = id;
42-
this.status = status;
43-
}
44-
45-
public Long getId() {
46-
return id;
47-
}
48-
49-
public String getStatus() {
50-
return status;
51-
}
52-
}
37+
// *** REMOVED THE INNER CLASS ***
5338
}

api-service/src/main/java/com/openpay/api/controller/TransactionController.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import org.springframework.http.ResponseEntity;
44
import org.springframework.web.bind.annotation.PostMapping;
55
import org.springframework.web.bind.annotation.RequestBody;
6+
import org.springframework.web.bind.annotation.RequestHeader;
67
import org.springframework.web.bind.annotation.RequestMapping;
78
import org.springframework.web.bind.annotation.RestController;
89

9-
import com.openpay.api.dto.PaymentRequest;
1010
import com.openpay.api.service.TransactionService;
11+
import com.openpay.shared.dto.PaymentRequest;
12+
import com.openpay.shared.dto.StatusResponse;
1113

1214
import jakarta.validation.Valid;
1315

@@ -22,8 +24,12 @@ public TransactionController(TransactionService transactionService) {
2224
}
2325

2426
@PostMapping
25-
public ResponseEntity<String> initiatePayment(@Valid @RequestBody PaymentRequest request) {
26-
Long id = transactionService.createTransaction(request);
27-
return ResponseEntity.ok("Transaction queued with ID: " + id);
27+
public ResponseEntity<StatusResponse> initiatePayment(
28+
@Valid @RequestBody PaymentRequest request,
29+
@RequestHeader("Idempotency-Key") String idempotencyKey) {
30+
31+
Long id = transactionService.createTransaction(request, idempotencyKey);
32+
StatusResponse response = new StatusResponse(id, "QUEUED", "Transaction queued");
33+
return ResponseEntity.ok(response);
2834
}
2935
}

api-service/src/main/java/com/openpay/api/handler/GlobalExceptionHandler.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import org.springframework.web.bind.annotation.ExceptionHandler;
1212
import org.springframework.web.bind.annotation.RestControllerAdvice;
1313

14+
import com.openpay.shared.exception.InvalidUpiException;
15+
import com.openpay.shared.exception.OpenPayException;
16+
1417
@RestControllerAdvice
1518
public class GlobalExceptionHandler {
1619

@@ -49,4 +52,17 @@ public ResponseEntity<?> handleGeneric(Exception ex) {
4952
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
5053
.body(Map.of("error", "Internal server error"));
5154
}
55+
56+
// Custom Exception Handlers for invalid upi and openpay
57+
58+
@ExceptionHandler(InvalidUpiException.class)
59+
public ResponseEntity<?> handleInvalidUpi(InvalidUpiException ex) {
60+
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
61+
}
62+
63+
@ExceptionHandler(OpenPayException.class)
64+
public ResponseEntity<?> handleOpenPay(OpenPayException ex) {
65+
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
66+
}
67+
5268
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.openpay.api.repository;
22

3-
import com.openpay.api.model.IdempotencyKeyEntity;
43
import org.springframework.data.jpa.repository.JpaRepository;
54

6-
public interface IdempotencyKeyRepository extends JpaRepository<IdempotencyKeyEntity, String> {}
5+
import com.openpay.api.model.IdempotencyKeyEntity;
6+
7+
public interface IdempotencyKeyRepository extends JpaRepository<IdempotencyKeyEntity, String> {
8+
boolean existsByIdempotencyKey(String idempotencyKey);
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.openpay.api.service;
2+
3+
public interface IdempotencyService {
4+
boolean isDuplicate(String key);
5+
void saveKey(String key, Long transactionId);
6+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.openpay.api.service;
2+
3+
import java.time.LocalDateTime;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import com.openpay.api.model.IdempotencyKeyEntity;
9+
import com.openpay.api.repository.IdempotencyKeyRepository;
10+
11+
@Service
12+
public class IdempotencyServiceImpl implements IdempotencyService {
13+
private final IdempotencyKeyRepository idempotencyKeyRepository;
14+
15+
public IdempotencyServiceImpl(IdempotencyKeyRepository idempotencyKeyRepository) {
16+
this.idempotencyKeyRepository = idempotencyKeyRepository;
17+
}
18+
19+
@Override
20+
public boolean isDuplicate(String key) {
21+
return idempotencyKeyRepository.existsByIdempotencyKey(key);
22+
}
23+
24+
@Override
25+
@Transactional
26+
public void saveKey(String key, Long transactionId) {
27+
IdempotencyKeyEntity entity = new IdempotencyKeyEntity();
28+
entity.setIdempotencyKey(key);
29+
entity.setTransactionId(transactionId);
30+
entity.setCreatedAt(LocalDateTime.now());
31+
idempotencyKeyRepository.save(entity);
32+
}
33+
}
Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
package com.openpay.api.service;
22

3-
import com.openpay.api.dto.PaymentRequest;
3+
import java.time.LocalDateTime;
4+
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.stereotype.Service;
8+
49
import com.openpay.api.model.TransactionEntity;
510
import com.openpay.api.repository.TransactionRepository;
6-
import org.springframework.stereotype.Service;
11+
import com.openpay.shared.dto.PaymentRequest;
12+
import com.openpay.shared.exception.OpenPayException;
713

8-
import java.time.LocalDateTime;
914

1015
@Service
1116
public class TransactionService {
1217

1318
private final TransactionRepository transactionRepository;
14-
15-
public TransactionService(TransactionRepository transactionRepository) {
19+
private final IdempotencyService idempotencyService;
20+
private static final Logger log = LoggerFactory.getLogger(TransactionService.class);
21+
public TransactionService(TransactionRepository transactionRepository, IdempotencyService idempotencyService) {
1622
this.transactionRepository = transactionRepository;
23+
this.idempotencyService = idempotencyService;
1724
}
1825

19-
public Long createTransaction(PaymentRequest request) {
26+
public Long createTransaction(PaymentRequest request, String idempotencyKey) {
2027
if (request.getSenderUpi().equalsIgnoreCase(request.getReceiverUpi())) {
21-
throw new IllegalArgumentException("Sender and receiver UPI must be different");
28+
throw new OpenPayException("Sender and receiver UPI must be different");
29+
}
30+
log.info("Creating transaction for sender={} receiver={}", request.getSenderUpi(), request.getReceiverUpi());
31+
// Idempotency check
32+
if (idempotencyService.isDuplicate(idempotencyKey)) {
33+
throw new OpenPayException("Duplicate request");
2234
}
23-
24-
// Stub: idempotency check will go here
2535

2636
TransactionEntity entity = new TransactionEntity();
2737
entity.setSenderUpi(request.getSenderUpi());
@@ -30,6 +40,10 @@ public Long createTransaction(PaymentRequest request) {
3040
entity.setStatus("queued");
3141
entity.setCreatedAt(LocalDateTime.now());
3242

43+
// Store idempotency key after successful transaction save
44+
TransactionEntity saved = transactionRepository.save(entity);
45+
idempotencyService.saveKey(idempotencyKey, saved.getId());
46+
3347
return transactionRepository.save(entity).getId();
3448
}
3549
}

0 commit comments

Comments
 (0)