[220402] 나머지 개발 완료
- purchase 도메인 command, query 개발 - payment 도메인 command, query 개발 - readme update
This commit is contained in:
76
README.md
76
README.md
@@ -1,52 +1,94 @@
|
||||
# Payment System with DDD
|
||||
|
||||
## Project Settings
|
||||
|
||||
### 1. Module Guide
|
||||
본 프로젝트는 CQRS 패턴을 이용하여 구현되어 있다.
|
||||
이에 따라 gradle을 이용하여 모듈이 둘로 나뉘어져 있는 디렉토리 구조를 확인할 수 있다.
|
||||
서비스를 위해서는 `payment-command` 모듈과 `payment-query` 모듈을 각각 실행해야 한다.
|
||||
|
||||
본 프로젝트는 CQRS 패턴을 이용하여 구현되어 있다. 이에 따라 gradle을 이용하여 모듈이 둘로 나뉘어져 있는 디렉토리 구조를 확인할 수 있다. 서비스를 위해서는 `payment-command` 모듈과 `payment-query` 모듈을 각각 실행해야 한다.
|
||||
|
||||
#### payment-command 모듈
|
||||
CRUD 중에서 CUD 만을 담당하는 모듈이다.
|
||||
본인의 테이블에 DB를 저장하며, 해당 이벤트를 Kafka에 발행하여 Query 모듈에 알려준다.
|
||||
DDD의 핵심 도메인들은 이곳에 구현이 된다.
|
||||
|
||||
CRUD 중에서 CUD 만을 담당하는 모듈이다. 본인의 테이블에 DB를 저장하며, 해당 이벤트를 Kafka에 발행하여 Query 모듈에 알려준다. DDD의 핵심 도메인들은 이곳에 구현이 된다.
|
||||
|
||||
#### payment-query 모듈
|
||||
CRUD 중에서 R 만을 담당하는 모듈이다.
|
||||
payment-command에서 발행한 이벤트를 Kafka로부터 consume하여 본인의 DB를 업데이트 한다.
|
||||
이 모듈은 DDD의 핵심 도메인 중심이 아닌, 클라이언트에게 필요한 정보를 제공한다. 위의 command 모듈과는 다르게 조회의 효율성, 클라이언트의 호출과 관련하여 조회에 유연하게 구성해야 한다.
|
||||
|
||||
CRUD 중에서 R 만을 담당하는 모듈이다. payment-command에서 발행한 이벤트를 Kafka로부터 consume하여 본인의 DB를 업데이트 한다. 이 모듈은 DDD의 핵심 도메인 중심이 아닌, 클라이언트에게 필요한 정보를 제공한다. 위의 command
|
||||
모듈과는 다르게 조회의 효율성, 클라이언트의 호출과 관련하여 조회에 유연하게 구성해야 한다.
|
||||
|
||||
### 2. Infra Used
|
||||
|
||||
```
|
||||
1. MySQL 8.0.21 for Command
|
||||
2. MySQL 8.0.21 for Query
|
||||
3. Kafka(Zookeeper)
|
||||
```
|
||||
위의 인프라는 로컬 실행을 위해서 docker-compose를 이용하였다.
|
||||
|
||||
> :warning: m1 맥북의 경우 docker-compose.yaml 파일에서 platform 관련 주석을 해제하고 실행할 것
|
||||
위의 인프라는 로컬 실행을 위해서 docker-compose를 이용하였다.
|
||||
|
||||
> :warning: m1 맥북의 경우 docker-compose.yaml 파일에서 platform 관련 주석을 해제하고 실행할 것
|
||||
|
||||
- CQRS에서 DB를 분리하는 방법은 여러가지가 있지만, 가장 보편적인(?) 방식인 물리 DB를 나누는 방식을 이용하였다.
|
||||
command와 query 모듈은 각각 다른 DB를 보게된다.
|
||||
command와 query 모듈은 각각 다른 DB를 보게된다.
|
||||
- kafka의 경우 application.yaml에 특별한 설정 없이도 kafka에 자동으로 붙게 설정이 되어 있다.(autoconfigure 덕분에)
|
||||
이외의 운영을 위한 상세 설정은 생략했다.
|
||||
이외의 운영을 위한 상세 설정은 생략했다.
|
||||
|
||||
## Domain 소개
|
||||
|
||||
이 프로젝트에서는 '결제' 컨텍스트를 예시로하는 간단한 DDD 프로젝트다.
|
||||
|
||||
1. 유저는 결제창에서 결제를 진행하여, 본인의 계정에 서비스 전용 재화(캐시)를 획득하게 된다. 게임이나 스타벅스 앱의 페이와 같이 충전과 관련된 시스템 대부분의 유형을 따르면 된다.
|
||||
2. 결제는 결제 대행사를 이용해서 처리하기 때문에 결제창에서 유저가 결제를 완료하면 본 도메인에서 제공하는 api로 결제 금액 정보가 담긴 결제 완료 요청을 보낸다. 결제창은 웹의 영역이므로 해당 도메인에서 다루지 않아도 무방하다.
|
||||
3. 서비스에는 특정 아이템이 있으며, 서비스 전용 재화를 이용해서 구매할 수 있다.(게임이라면 아이템, 스타벅스라면 구매한 식음료) 다만 아이템은 본 컨텍스트에서는 관리하지 않는다.
|
||||
|
||||
도메인은 대략 다음과 같은 구조를 따른다.
|
||||
|
||||
1. User: 유저는 캐시를 소유할 수 있어 내부 엔터티로 구현. 유저 계정 관리 서비스가 따로 있다고 가정한다. 여기에서는 자체적으로 생성한다.
|
||||
2. Payment: 캐시를 획득하는 행위
|
||||
3. Cash: Payment로부터 생성된 User에게 할당되는 Cash
|
||||
4. Purchase: 캐시를 사용하는 행위. 캐시를 차감한다.
|
||||
|
||||

|
||||
|
||||
## Use Case
|
||||
|
||||
### i. Command
|
||||
|
||||
- 외부 결제 시스템 결제로 인한 캐시 증가
|
||||
|
||||
```
|
||||
1. 해당 유저 아이디가 없을 경우 DB에 추가(계정은 다른 서비스에서 관리한다고 가정)
|
||||
2. 유저 잔액 업데이트
|
||||
2. 결제 기록 추가
|
||||
3. 해당 이벤트를 consumer로 publish
|
||||
```
|
||||
|
||||
- 각종 이유(구매 등)에 대한 캐시 사용
|
||||
|
||||
```
|
||||
1. 유저의 잔액 확인
|
||||
2. 유저의 잔액 차감
|
||||
3. 구매 기록 저장
|
||||
3. 해당 이벤트를 consumer로 publish
|
||||
```
|
||||
|
||||
### ii. Query
|
||||
- 유저의 결제 내역 단건/기간별 조회
|
||||
- 유저의 사용 내역 단건/기간별 조회
|
||||
|
||||
|
||||
|
||||
- 유저의 결제 내역 조회
|
||||
- 유저의 사용 내역 조회
|
||||
|
||||
#### Query Model의 모듈 특징
|
||||
|
||||
- Query 모델에서는 Consuming과 동시에 Query 모델에서 필요한 데이터로 가공한다고 가정한다.
|
||||
- 그러므로 몇 가지 요소가 생략될 수 있다.
|
||||
- Repository가 Controller에 직접 주입될 수 있다. 왜냐하면 DB 쿼리 외의 별다른 로직이 필요없기 때문이다.
|
||||
- 마찬가지 이유로 Response DTO도 사용하지 않는다. 이미 DB에 저장된 모든 정보가 Response에 최적화 시켜져 있기 때문이다.
|
||||
|
||||
#### Query Model의 모듈 특징
|
||||
|
||||
- Query 모델에서는 Consuming과 동시에 Query 모델에서 필요한 데이터로 가공한다고 가정한다.
|
||||
- 그러므로 몇 가지 요소가 생략될 수 있다.
|
||||
- Repository가 Controller에 직접 주입될 수 있다. 왜냐하면 DB 쿼리 외의 별다른 로직이 필요없기 때문이다.
|
||||
- 마찬가지 이유로 Response DTO도 사용하지 않는다. 이미 DB에 저장된 모든 정보가 Response에 최적화 시켜져 있기 때문이다.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
# platform: linux/amd64 # for m1 mac
|
||||
# platform: linux/amd64 # for m1 mac
|
||||
mysql-query:
|
||||
container_name: query-mysql
|
||||
image: mysql:8.0.21
|
||||
@@ -22,10 +22,10 @@ services:
|
||||
- MYSQL_ROOT_PASSWORD=payment
|
||||
- TZ=UTC
|
||||
ports:
|
||||
- "3307:3307"
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
# platform: linux/amd64 # for m1 mac
|
||||
- ./db2:/var/lib/mysql
|
||||
# platform: linux/amd64 # for m1 mac
|
||||
zookeeper:
|
||||
container_name: payment-zookeeper
|
||||
image: wurstmeister/zookeeper
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.example;
|
||||
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Service
|
||||
public class Listener {
|
||||
@KafkaListener(topics = "topic", groupId = "group1")
|
||||
public void consume(String message) {
|
||||
System.out.println("receive message : " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.common.event;
|
||||
|
||||
public interface DomainEvent {
|
||||
|
||||
String topic();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.common.event;
|
||||
|
||||
public interface EventPublisher {
|
||||
|
||||
void publish(DomainEvent event);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.common.infra;
|
||||
|
||||
import com.example.common.event.DomainEvent;
|
||||
import com.example.common.event.EventPublisher;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class KafkaEventPublisher implements EventPublisher {
|
||||
|
||||
private final KafkaTemplate<String, DomainEvent> kafkaProducer;
|
||||
|
||||
@Override
|
||||
public void publish(DomainEvent event) {
|
||||
String topic = event.topic();
|
||||
kafkaProducer.send(new ProducerRecord<>(topic, event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaConfig {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.payment.application;
|
||||
|
||||
import com.example.payment.infra.http.request.PaymentCompleteRequest;
|
||||
|
||||
public interface PaymentService {
|
||||
|
||||
String completePayment(PaymentCompleteRequest request);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.payment.application;
|
||||
|
||||
import com.example.common.event.EventPublisher;
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.payment.domain.PaymentRepository;
|
||||
import com.example.payment.domain.event.PaymentCreated;
|
||||
import com.example.payment.infra.http.request.PaymentCompleteRequest;
|
||||
import com.example.user.domain.User;
|
||||
import com.example.user.domain.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentServiceImpl implements PaymentService {
|
||||
|
||||
private final EventPublisher eventPublisher;
|
||||
private final UserRepository userRepository;
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public String completePayment(PaymentCompleteRequest request) {
|
||||
// 1. request validation
|
||||
request.validate();
|
||||
|
||||
// 2. 유저의 잔액 추가 / 캐시 생성
|
||||
User user = userRepository.getByIdOrDefault(request.getUserId());
|
||||
user.increaseBalance(request.getAmount());
|
||||
userRepository.save(user);
|
||||
|
||||
// 3. 결제 데이터 생성
|
||||
Payment payment = paymentRepository.save(Payment.of(request.getPgId(), request.getPayBy(), user, null, null));
|
||||
|
||||
// 4. 이벤트 발행
|
||||
eventPublisher.publish(PaymentCreated.builder()
|
||||
.request(request)
|
||||
.userId(user.getUserId())
|
||||
.paymentId(payment.getId())
|
||||
.createdAt(payment.getCreatedAt())
|
||||
.build());
|
||||
|
||||
return payment.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.example.payment.domain;
|
||||
|
||||
import com.example.user.domain.User;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class Payment {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String pgId;
|
||||
|
||||
private String payBy;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn
|
||||
private User user;
|
||||
|
||||
@CreatedDate
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
private ZonedDateTime updatedAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ZonedDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public static Payment of(String pgId, String payBy, User user, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
|
||||
return new Payment(UUID.randomUUID().toString(), pgId, payBy, user, createdAt, updatedAt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.payment.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PaymentRepository {
|
||||
|
||||
Payment save(Payment purchase);
|
||||
|
||||
Optional<Payment> getById(String id);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.example.payment.domain.event;
|
||||
|
||||
import com.example.common.event.DomainEvent;
|
||||
import com.example.payment.infra.http.request.PaymentCompleteRequest;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class PaymentCreated implements DomainEvent, Serializable {
|
||||
|
||||
private final String userId;
|
||||
|
||||
private final String paymentId;
|
||||
|
||||
private final BigDecimal amount;
|
||||
|
||||
private final String payBy;
|
||||
|
||||
private final String pgId;
|
||||
|
||||
private final ZonedDateTime createdAt;
|
||||
|
||||
@Override
|
||||
public String topic() {
|
||||
return "PaymentCreated";
|
||||
}
|
||||
|
||||
@Builder
|
||||
private PaymentCreated(PaymentCompleteRequest request, String userId, String paymentId, ZonedDateTime createdAt) {
|
||||
this.userId = userId;
|
||||
this.paymentId = paymentId;
|
||||
this.amount = request.getAmount();
|
||||
this.payBy = request.getPayBy();
|
||||
this.pgId = request.getPgId();
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.payment.infra.http;
|
||||
|
||||
import com.example.payment.application.PaymentService;
|
||||
import com.example.payment.infra.http.request.PaymentCompleteRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(("/payments"))
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentController {
|
||||
|
||||
private final PaymentService paymentService;
|
||||
|
||||
@PostMapping("/complete")
|
||||
public ResponseEntity<String> paymentComplete(@RequestBody PaymentCompleteRequest request) {
|
||||
return ResponseEntity.ok(paymentService.completePayment(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.example.payment.infra.http.request;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class PaymentCompleteRequest {
|
||||
|
||||
private String userId;
|
||||
private BigDecimal amount;
|
||||
private String payBy;
|
||||
private String pgId;
|
||||
|
||||
@Builder
|
||||
private PaymentCompleteRequest(String userId, BigDecimal amount, String payBy, String pgId) {
|
||||
this.userId = userId;
|
||||
this.amount = amount;
|
||||
this.payBy = payBy;
|
||||
this.pgId = pgId;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (userId == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (payBy == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (pgId == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.payment.infra.persistence;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface PaymentJpaRepository extends CrudRepository<Payment, String> {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.payment.infra.persistence;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.payment.domain.PaymentRepository;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentRepositoryImpl implements PaymentRepository {
|
||||
|
||||
private final PaymentJpaRepository jpaRepository;
|
||||
|
||||
@Override
|
||||
public Payment save(Payment payment) {
|
||||
return jpaRepository.save(payment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Payment> getById(String id) {
|
||||
return jpaRepository.findById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.purchase.application;
|
||||
|
||||
import com.example.purchase.infra.http.request.PurchaseRequest;
|
||||
|
||||
public interface PurchaseService {
|
||||
|
||||
String purchase(PurchaseRequest request);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.example.purchase.application;
|
||||
|
||||
import com.example.common.event.EventPublisher;
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import com.example.purchase.domain.event.PurchaseCreated;
|
||||
import com.example.purchase.infra.http.request.PurchaseRequest;
|
||||
import com.example.user.domain.User;
|
||||
import com.example.user.domain.UserRepository;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PurchaseServiceImpl implements PurchaseService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PurchaseRepository purchaseRepository;
|
||||
private final EventPublisher eventPublisher;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public String purchase(PurchaseRequest request) {
|
||||
// 1. request validation
|
||||
request.validate();
|
||||
|
||||
// 2. 유저 존재 여부 확인
|
||||
User foundUser = userRepository.findById(request.getUserId()).orElseThrow(NoSuchElementException::new);
|
||||
|
||||
// 3. 잔액 여유 확인
|
||||
if (!foundUser.hasMoreThan(request.getAmount())) {
|
||||
throw new IllegalArgumentException(); // TODO 커스텀 Exception으로 변경
|
||||
}
|
||||
|
||||
// 4. 캐시 차감
|
||||
foundUser.useCash(request.getAmount());
|
||||
userRepository.save(foundUser);
|
||||
|
||||
// 5. 구매 기록 추가
|
||||
Purchase purchase = purchaseRepository.save(Purchase.of(foundUser, request.getType(), request.getAmount(), null, null));
|
||||
|
||||
// 6. 이벤트 발행
|
||||
eventPublisher.publish(PurchaseCreated.builder()
|
||||
.userId(foundUser.getUserId())
|
||||
.purchaseId(purchase.getId())
|
||||
.type(request.getType())
|
||||
.amount(request.getAmount())
|
||||
.createdAt(purchase.getCreatedAt())
|
||||
.build());
|
||||
|
||||
return purchase.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.user.domain.User;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class Purchase {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn
|
||||
private User user;
|
||||
|
||||
private PurchaseType type;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
@CreatedDate
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
private ZonedDateTime updatedAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ZonedDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public static Purchase of(User user, PurchaseType type, BigDecimal amount, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
|
||||
return new Purchase(UUID.randomUUID().toString(), user, type, amount, createdAt, updatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PurchaseRepository {
|
||||
|
||||
Purchase save(Purchase purchase);
|
||||
|
||||
Optional<Purchase> getById(String id);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
public enum PurchaseType {
|
||||
ITEM
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.example.purchase.domain.event;
|
||||
|
||||
import com.example.common.event.DomainEvent;
|
||||
import com.example.purchase.domain.PurchaseType;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class PurchaseCreated implements DomainEvent, Serializable {
|
||||
|
||||
private final String userId;
|
||||
|
||||
private final String purchaseId;
|
||||
|
||||
private final PurchaseType type;
|
||||
|
||||
private final BigDecimal amount;
|
||||
|
||||
private final ZonedDateTime createdAt;
|
||||
|
||||
@Override
|
||||
public String topic() {
|
||||
return "PurchaseCreated";
|
||||
}
|
||||
|
||||
@Builder
|
||||
private PurchaseCreated(String userId, String purchaseId, PurchaseType type, BigDecimal amount, ZonedDateTime createdAt) {
|
||||
this.userId = userId;
|
||||
this.purchaseId = purchaseId;
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.purchase.infra.http;
|
||||
|
||||
import com.example.purchase.application.PurchaseService;
|
||||
import com.example.purchase.infra.http.request.PurchaseRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/purchases")
|
||||
@RequiredArgsConstructor
|
||||
public class PurchaseController {
|
||||
|
||||
private final PurchaseService purchaseService;
|
||||
|
||||
@PostMapping()
|
||||
public ResponseEntity<String> purchase(@RequestBody PurchaseRequest request) {
|
||||
return ResponseEntity.ok(purchaseService.purchase(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.purchase.infra.http.request;
|
||||
|
||||
import com.example.purchase.domain.PurchaseType;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class PurchaseRequest {
|
||||
|
||||
private String userId;
|
||||
private PurchaseType type;
|
||||
private BigDecimal amount;
|
||||
|
||||
@Builder
|
||||
private PurchaseRequest(String userId, PurchaseType type, BigDecimal amount) {
|
||||
this.userId = userId;
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (userId == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.purchase.infra.persistence;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface PurchaseJpaRepository extends CrudRepository<Purchase, String> {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.purchase.infra.persistence;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class PurchaseRepositoryImpl implements PurchaseRepository {
|
||||
|
||||
private final PurchaseJpaRepository jpaRepository;
|
||||
|
||||
@Override
|
||||
public Purchase save(Purchase purchase) {
|
||||
return jpaRepository.save(purchase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Purchase> getById(String id) {
|
||||
return jpaRepository.findById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.example.user.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
private String userId;
|
||||
|
||||
private BigDecimal balance;
|
||||
|
||||
@CreatedDate
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void increaseBalance(BigDecimal amount) {
|
||||
balance = balance.add(amount);
|
||||
}
|
||||
|
||||
public boolean hasMoreThan(BigDecimal amount) {
|
||||
return balance.compareTo(amount) > 0;
|
||||
}
|
||||
|
||||
private User(String userId, BigDecimal balance) {
|
||||
this.userId = userId;
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public static User newUser(String userId) {
|
||||
return new User(userId, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
public void useCash(BigDecimal amount) {
|
||||
balance = balance.subtract(amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.user.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository {
|
||||
|
||||
User getByIdOrDefault(String id);
|
||||
|
||||
String save(User user);
|
||||
|
||||
Optional<User> findById(String id);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.user.infra.persistence;
|
||||
|
||||
import com.example.user.domain.User;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface UserJpaRepositroy extends CrudRepository<User, String> {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.user.infra.persistence;
|
||||
|
||||
import com.example.user.domain.User;
|
||||
import com.example.user.domain.UserRepository;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class UserRepositoryImpl implements UserRepository {
|
||||
|
||||
private final UserJpaRepositroy jpaRepository;
|
||||
|
||||
@Override
|
||||
public String save(User user) {
|
||||
return jpaRepository.save(user).getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getByIdOrDefault(String id) {
|
||||
Optional<User> found = jpaRepository.findById(id);
|
||||
return found.orElse(jpaRepository.save(User.newUser(id)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(String id) {
|
||||
return jpaRepository.findById(id);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,15 @@ spring:
|
||||
password: payment
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect
|
||||
kafka:
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type: TRACE
|
||||
@@ -1,18 +1,116 @@
|
||||
package com.example.payment;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
|
||||
import com.example.common.event.DomainEvent;
|
||||
import com.example.common.event.EventPublisher;
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.payment.domain.PaymentRepository;
|
||||
import com.example.payment.infra.http.request.PaymentCompleteRequest;
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import com.example.purchase.domain.PurchaseType;
|
||||
import com.example.purchase.infra.http.request.PurchaseRequest;
|
||||
import com.example.user.domain.User;
|
||||
import com.example.user.domain.UserRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class PaymentApplicationTests {
|
||||
|
||||
@Autowired
|
||||
KafkaTemplate<String, String> template;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean // 주석처리하고 코드에서도 해당 mock을 주석처리 할 경우 메시지 전송이 실제로 이루어짐
|
||||
private EventPublisher eventPublisher;
|
||||
|
||||
@MockBean
|
||||
private UserRepository userRepository;
|
||||
|
||||
@MockBean
|
||||
private PaymentRepository paymentRepository;
|
||||
|
||||
@MockBean
|
||||
private PurchaseRepository purchaseRepository;
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.mapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
template.send("topic", "hello this is new topic2!!!!!!");
|
||||
void paymentCompleteControllerTest() throws Exception {
|
||||
|
||||
String testUserId = "test-user-id";
|
||||
|
||||
PaymentCompleteRequest request = PaymentCompleteRequest.builder()
|
||||
.userId(testUserId)
|
||||
.amount(BigDecimal.valueOf(1000))
|
||||
.payBy("card")
|
||||
.pgId("example-pg")
|
||||
.build();
|
||||
User actualUser = User.newUser(testUserId);
|
||||
|
||||
Payment payment = Payment.of("example-pg", "card", actualUser, ZonedDateTime.now(), ZonedDateTime.now());
|
||||
|
||||
when(userRepository.getByIdOrDefault(anyString())).thenReturn(actualUser);
|
||||
when(userRepository.save(any(User.class))).thenReturn("test-user-id");
|
||||
when(paymentRepository.save(any(Payment.class))).thenReturn(payment);
|
||||
doNothing().when(eventPublisher).publish(any(DomainEvent.class));
|
||||
|
||||
mockMvc.perform(post("/payments/complete")
|
||||
.contentType("application/json")
|
||||
.content(mapper.writeValueAsString(request)))
|
||||
.andDo(print())
|
||||
.andExpect(content().string(payment.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void purchaseControllerTest() throws Exception {
|
||||
|
||||
String testUserId = "test-user-id";
|
||||
|
||||
PurchaseRequest request = PurchaseRequest.builder()
|
||||
.userId("test-user-id")
|
||||
.type(PurchaseType.ITEM)
|
||||
.amount(BigDecimal.valueOf(1000))
|
||||
.build();
|
||||
|
||||
User newUser = User.newUser(testUserId);
|
||||
newUser.increaseBalance(BigDecimal.valueOf(10000));
|
||||
Optional<User> actualUser = Optional.of(newUser);
|
||||
|
||||
Purchase purchase = Purchase.of(actualUser.get(), request.getType(), request.getAmount(), ZonedDateTime.now(), ZonedDateTime.now());
|
||||
|
||||
when(userRepository.findById(anyString())).thenReturn(actualUser);
|
||||
when(userRepository.save(any(User.class))).thenReturn("test-user-id");
|
||||
when(purchaseRepository.save(any(Purchase.class))).thenReturn(purchase);
|
||||
doNothing().when(eventPublisher).publish(any(DomainEvent.class));
|
||||
|
||||
mockMvc.perform(post("/purchases")
|
||||
.contentType("application/json")
|
||||
.content(mapper.writeValueAsString(request)))
|
||||
.andDo(print())
|
||||
.andExpect(content().string(purchase.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package java.com.example;
|
||||
package com.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.payment.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Payment {
|
||||
|
||||
@Id
|
||||
private String paymentId;
|
||||
|
||||
private String userId;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private String payBy;
|
||||
|
||||
private String pgId;
|
||||
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
@Builder
|
||||
private Payment(String paymentId, String userId, BigDecimal amount, String payBy, String pgId, ZonedDateTime createdAt) {
|
||||
this.paymentId = paymentId;
|
||||
this.userId = userId;
|
||||
this.amount = amount;
|
||||
this.payBy = payBy;
|
||||
this.pgId = pgId;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.payment.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PaymentRepository {
|
||||
|
||||
Payment save(Payment payment);
|
||||
|
||||
List<Payment> usersPayments(String userId);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.payment.domain.event;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
@ToString
|
||||
public class PaymentCreated {
|
||||
|
||||
private String userId;
|
||||
|
||||
private String paymentId;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private String payBy;
|
||||
|
||||
private String pgId;
|
||||
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
public Payment toPayment() {
|
||||
return Payment.builder()
|
||||
.userId(userId)
|
||||
.paymentId(paymentId)
|
||||
.amount(amount)
|
||||
.payBy(payBy)
|
||||
.pgId(pgId)
|
||||
.createdAt(createdAt)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.payment.infra.http;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.payment.domain.PaymentRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/payments")
|
||||
public class PaymentController {
|
||||
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public List<Payment> userPayments(@PathVariable("userId") String userId) {
|
||||
return paymentRepository.usersPayments(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.payment.infra.mysql;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface PaymentJpaRepository extends CrudRepository<Payment, String> {
|
||||
|
||||
@Query("select p from Payment p where p.userId = :userId")
|
||||
List<Payment> findByUserId(@Param("userId") String userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.payment.infra.mysql;
|
||||
|
||||
import com.example.payment.domain.Payment;
|
||||
import com.example.payment.domain.PaymentRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentRepositoryImpl implements PaymentRepository {
|
||||
|
||||
private final PaymentJpaRepository jpaRepository;
|
||||
|
||||
@Override
|
||||
public Payment save(Payment payment) {
|
||||
return jpaRepository.save(payment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Payment> usersPayments(String userId) {
|
||||
return jpaRepository.findByUserId(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Purchase {
|
||||
|
||||
@Id
|
||||
private String purchaseId;
|
||||
|
||||
private String userId;
|
||||
|
||||
private PurchaseType type;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
@Builder
|
||||
private Purchase(String purchaseId, String userId, PurchaseType type, BigDecimal amount, ZonedDateTime createdAt) {
|
||||
this.purchaseId = purchaseId;
|
||||
this.userId = userId;
|
||||
this.type = type;
|
||||
this.amount = amount;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PurchaseRepository {
|
||||
|
||||
Purchase save(Purchase purchase);
|
||||
|
||||
List<Purchase> usersPurchase(String userId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.purchase.domain;
|
||||
|
||||
public enum PurchaseType {
|
||||
ITEM
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.purchase.domain.event;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseType;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@ToString
|
||||
public class PurchaseCreated {
|
||||
|
||||
private String userId;
|
||||
|
||||
private String purchaseId;
|
||||
|
||||
private PurchaseType type;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private ZonedDateTime createdAt;
|
||||
|
||||
public Purchase toPurchase() {
|
||||
return Purchase.builder()
|
||||
.purchaseId(purchaseId)
|
||||
.userId(userId)
|
||||
.type(type)
|
||||
.amount(amount)
|
||||
.createdAt(createdAt)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.purchase.infra.http;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/purchases")
|
||||
public class PurchaseController {
|
||||
|
||||
private final PurchaseRepository purchaseRepository;
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public List<Purchase> userPurchases(@PathVariable("userId") String userId) {
|
||||
return purchaseRepository.usersPurchase(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.purchase.infra.kafka;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import com.example.purchase.domain.event.PurchaseCreated;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PurchaseCreatedListener {
|
||||
|
||||
private final PurchaseRepository purchaseRepository;
|
||||
|
||||
@KafkaListener(topics = "PurchaseCreated", groupId = "group1")
|
||||
public void consume(PurchaseCreated message) {
|
||||
log.info("event received: {}", message);
|
||||
Purchase purchase = message.toPurchase();
|
||||
purchaseRepository.save(purchase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.purchase.infra.mysql;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface PurchaseJpaRepository extends CrudRepository<Purchase, String> {
|
||||
|
||||
@Query("select p from Purchase p where p.userId = :userId")
|
||||
List<Purchase> findByUserId(@Param("userId") String userId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.purchase.infra.mysql;
|
||||
|
||||
import com.example.purchase.domain.Purchase;
|
||||
import com.example.purchase.domain.PurchaseRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class PurchaseRepositoryImpl implements PurchaseRepository {
|
||||
|
||||
private final PurchaseJpaRepository jpaRepository;
|
||||
|
||||
@Override
|
||||
public Purchase save(Purchase purchase) {
|
||||
return jpaRepository.save(purchase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Purchase> usersPurchase(String userId) {
|
||||
return jpaRepository.findByUserId(userId);
|
||||
}
|
||||
}
|
||||
23
payment-query/src/main/resources/application.yaml
Normal file
23
payment-query/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
server:
|
||||
port: 8082
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3307/payment?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: payment
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect
|
||||
kafka:
|
||||
consumer:
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring:
|
||||
json:
|
||||
trusted:
|
||||
packages: '*'
|
||||
@@ -1,14 +0,0 @@
|
||||
server:
|
||||
port: 8082
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3307/payment?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: payment
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect
|
||||
Reference in New Issue
Block a user