Skip to content

Commit 8a81fed

Browse files
Merge pull request #35 from WON-Q/feat/30
Feat: 함
2 parents 929b8d3 + 506c4be commit 8a81fed

15 files changed

Lines changed: 659 additions & 1 deletion

File tree

docs/payment-flow.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# 결제 Flow
2+
3+
```mermaid
4+
sequenceDiagram
5+
participant 사용자 as 사용자
6+
participant 앱카드 as 앱카드
7+
participant 원큐 오더 웹 as 원큐 오더 웹
8+
participant 원큐 오더 서버 as 원큐 오더 서버
9+
participant pg사 as PG
10+
participant 앱카드 서버 as 앱카드 서버
11+
participant 카드사 as 카드사
12+
participant 은행 as 은행
13+
사용자 ->> 원큐 오더 웹: 1. "결제하기" 클릭
14+
원큐 오더 웹 ->> 원큐 오더 서버: 2. 주문 생성 요청
15+
원큐 오더 서버 ->> 원큐 오더 서버: 3. orderId 생성
16+
원큐 오더 서버 ->> 원큐 오더 웹: 4. orderId, amount, merchantId 반환
17+
원큐 오더 웹 ->> pg사: 5. 결제 준비 요청
18+
pg사 ->> pg사: 6. paymentId 생성 및 저장
19+
pg사 ->> 원큐 오더 웹: 7. paymentId 응답
20+
원큐 오더 웹 ->> 사용자: 8. 결제 수단 선택 UI 표시
21+
사용자 ->> 원큐 오더 웹: 9. "우리카드" 선택
22+
원큐 오더 웹 ->> pg사: 10. 결제 수단 선택 요청
23+
pg사 ->> pg사: 11. PG UI redirectUrl 생성
24+
pg사 ->> 원큐 오더 웹: 12. redirectUrl 응답
25+
원큐 오더 웹 ->> 사용자: 13. PG UI 화면 노출 (WOORI Pay, 앱 없이 결제 버튼 등)
26+
사용자 ->> pg사: 14. "WOORI Pay 결제" 클릭
27+
pg사 ->> pg사: 15. 결제 방식 선택 이벤트 전달
28+
pg사 ->> 앱카드 서버: 16. 결제 인증 API 호출
29+
앱카드 서버 ->> 앱카드 서버: 17. 인증 세션 시작
30+
앱카드 서버 ->> 앱카드 서버: 18. 인증 및 서명에 사용되는 챌린지 생성<br/>challenge="abcde" 생성
31+
note right of 앱카드 서버: 캐시에 트랜잭션 ID와 챌린지 저장
32+
앱카드 서버 ->> pg사: 19. 딥 링크 반환
33+
pg사 ->> 원큐 오더 서버: 20. 딥 링크 포함 결제 시작 응답 반환
34+
원큐 오더 서버 ->> 원큐 오더 웹: 21. 딥 링크를 원큐 오더 웹에 전달
35+
원큐 오더 웹 ->> 사용자: 22. 사용자에게 "앱카드에서 결제를 진행해주세요" 메시지와 함께 딥 링크 실행 버튼 표시
36+
사용자 ->> 앱카드: 23. 딥 링크를 열어 앱카드 실행
37+
앱카드 ->> 앱카드 서버: 24. 인증 및 서명을 위한 챌린지 요청
38+
앱카드 서버 ->> 앱카드: 25. 챌린지(임의의 문자열) 반환
39+
앱카드 ->> 사용자: 26. 인증을 위한 Face ID 요청
40+
사용자 ->> 앱카드: 27. 얼굴 인식
41+
앱카드 ->> 앱카드: 28. 챌린지 서명<br/>개인키로 signature = "sign(abcde)" 생성
42+
앱카드 ->> 앱카드 서버: 29. 서명 제출
43+
앱카드 서버 ->> 앱카드 서버: 30. 공개 키로 서명 검증
44+
45+
alt 서명 성공
46+
앱카드 서버 ->> 앱카드 서버: 31. 상태: AUTHENTICATED
47+
앱카드 서버 ->> pg사: 32. 결제 승인 요청 (txnId, cardNumber 등)
48+
49+
alt 신용카드
50+
pg사 ->> 카드사: 33. 승인 요청
51+
카드사 ->> 카드사: 34. 유효성 및 한도 확인
52+
alt 승인 성공
53+
카드사 ->> 카드사: 35. 결제 예약
54+
카드사 ->> pg사: 36. 승인 응답
55+
else 승인 실패
56+
카드사 ->> pg사: 36. 실패 응답
57+
end
58+
else 체크카드
59+
pg사 ->> 카드사: 33. 승인 요청
60+
카드사 ->> 은행: 34. 잔액 확인 및 차감
61+
alt 승인 성공
62+
은행 ->> 카드사: 35. 성공 응답
63+
카드사 ->> pg사: 36. 성공 응답
64+
else 실패
65+
은행 ->> 카드사: 35. 실패 응답
66+
카드사 ->> pg사: 36. 실패 응답
67+
end
68+
end
69+
70+
pg사 ->> pg사: 37. 결제 상태 업데이트
71+
pg사 ->> 앱카드 서버: 38. 결제 결과 전송
72+
앱카드 서버 ->> 앱카드: 39. 결제 결과 전송
73+
앱카드 ->> 사용자: 40. 결제 완료 화면 표시 및<br/>돌아가서 결제를 완료해주세요 페이지 표시
74+
사용자 ->> 원큐 오더 웹: 41. 원큐 오더 웹으로 돌아감
75+
원큐 오더 웹 ->> 원큐 오더 서버: 42. 주문 결제 검증 요청
76+
원큐 오더 서버 ->> 원큐 오더 서버: 43. 주문 결제 검증
77+
원큐 오더 서버 ->> 원큐 오더 웹: 44. 주문 결제 검증 응답
78+
원큐 오더 웹 ->> 사용자: 45. 최종 주문 완료
79+
else 서명 실패
80+
앱카드 서버 ->> 앱카드: 31. 인증 실패 응답
81+
앱카드 ->> 사용자: 32. 인증 실패 화면 표시
82+
앱카드 서버 ->> pg사: 33. 인증 실패 알림
83+
pg사 ->> 원큐 오더 서버: 34. 실패 응답
84+
원큐 오더 서버 ->> 원큐 오더 서버: 35. 주문 상태 AUTH_FAILED
85+
원큐 오더 서버 ->> 원큐 오더 웹: 36. 실패 응답
86+
원큐 오더 웹 ->> 사용자: 37. 결제 실패 화면 표시
87+
end
88+
89+
note over 카드사, 은행: (신용카드 결제일 기준 자금 청구 발생)
90+
카드사 ->> 카드사: 48. 청구 집계
91+
카드사 ->> 은행: 49. 자금 청구
92+
은행 ->> 은행: 50. 계좌 차감
93+
은행 ->> 카드사: 51. 청구 완료 응답
94+
카드사 ->> 카드사: 52. 청구 상태 업데이트
95+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package com.fisa.wonq.global.config;
22

3+
import feign.Logger;
34
import org.springframework.cloud.openfeign.EnableFeignClients;
5+
import org.springframework.context.annotation.Bean;
46
import org.springframework.context.annotation.Configuration;
57

68
@Configuration
79
@EnableFeignClients(basePackages = "com.fisa.wonq")
810
public class FeignConfig {
911

12+
@Bean
13+
Logger.Level feignLoggerLevel() {
14+
return Logger.Level.FULL;
15+
}
16+
1017
}

src/main/java/com/fisa/wonq/global/security/config/WebSecurityConfig.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.core.annotation.Order;
1011
import org.springframework.security.authentication.AuthenticationManager;
1112
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
1213
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -51,6 +52,7 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration a
5152
* permitAll 권한을 가진 엔드포인트에 적용되는 SecurityFilterChain
5253
*/
5354
@Bean
55+
@Order(1)
5456
public SecurityFilterChain securityFilterChainPermitAll(HttpSecurity http) throws Exception {
5557
configureCommonSecuritySettings(http);
5658
http.securityMatchers(matchers -> matchers.requestMatchers(requestPermitAll()))
@@ -64,6 +66,7 @@ public SecurityFilterChain securityFilterChainPermitAll(HttpSecurity http) throw
6466
* 인증 및 인가가 필요한 엔드포인트에 적용되는 SecurityFilterChain 입니다.
6567
*/
6668
@Bean
69+
@Order(2)
6770
public SecurityFilterChain securityFilterChainAuthorized(HttpSecurity http) throws Exception {
6871
configureCommonSecuritySettings(http);
6972
http
@@ -86,6 +89,7 @@ public SecurityFilterChain securityFilterChainAuthorized(HttpSecurity http) thro
8689
* 위에서 정의된 엔드포인트 이외에는 authenticated로 설정
8790
*/
8891
@Bean
92+
@Order(3)
8993
public SecurityFilterChain securityFilterChainDefault(HttpSecurity http) throws Exception {
9094
configureCommonSecuritySettings(http);
9195
http
@@ -139,7 +143,8 @@ private RequestMatcher[] requestPermitAll() {
139143
antMatcher("/api/v1/orders/prepare"),
140144
antMatcher("/api/v1/merchant/{merchantId}/overview"),
141145
antMatcher("/api/v1/orders/code/{orderCode}"),
142-
antMatcher("/api/orders/verify")
146+
antMatcher("/api/orders/verify"),
147+
antMatcher("/api/orders/refund")
143148
);
144149
return requestMatchers.toArray(RequestMatcher[]::new);
145150
}

src/main/java/com/fisa/wonq/merchant/domain/Merchant.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public void addMenu(Menu menu) {
8989
menu.setMerchant(this);
9090
}
9191

92+
9293
public void setMember(Member member) {
9394
this.member = member;
9495
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.fisa.wonq.order.controller;
2+
3+
import com.fisa.wonq.order.controller.dto.req.OrderRefundRequestDto;
4+
import com.fisa.wonq.order.controller.dto.res.OrderRefundResponseDto;
5+
import com.fisa.wonq.order.service.OrderRefundService;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@Slf4j
15+
@RestController
16+
@RequiredArgsConstructor
17+
@RequestMapping("/api/orders")
18+
public class OrderRefundController {
19+
20+
private final OrderRefundService orderRefundService;
21+
22+
@PostMapping("/refund")
23+
public ResponseEntity<OrderRefundResponseDto> refundOrder(
24+
@RequestBody OrderRefundRequestDto request) {
25+
26+
log.info("[Controller] 주문 환불 요청: orderCode={}, paymentId={}",
27+
request.getOrderCode(), request.getPaymentId());
28+
29+
OrderRefundResponseDto response = orderRefundService.refundOrder(request);
30+
31+
log.info("[Controller] 주문 환불 응답: orderCode={}, orderStatus={}, message={}",
32+
response.getOrderCode(), response.getOrderStatus(), response.getMessage());
33+
34+
return ResponseEntity.ok(response);
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.fisa.wonq.order.controller.dto.req;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
/**
8+
* 원큐오더 클라이언트로부터 환불 요청을 받는 DTO
9+
*/
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class OrderRefundRequestDto {
14+
15+
/**
16+
* 환불 요청에 대한 주문 코드
17+
* 예: 240405T1017_t3
18+
*/
19+
private String orderCode;
20+
21+
/**
22+
* 환불 요청에 대한 결제 ID
23+
* 예: STUB_TXN_abc123456
24+
*/
25+
private String paymentId;
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.fisa.wonq.order.controller.dto.res;
2+
3+
import com.fisa.wonq.order.domain.enums.OrderStatus;
4+
import com.fisa.wonq.order.domain.enums.PaymentStatus;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
/**
9+
* 원큐오더 서버가 원큐오더 클라이언트로 환불 응답을 보내는 DTO
10+
*/
11+
@Getter
12+
@Builder
13+
public class OrderRefundResponseDto {
14+
15+
16+
private String orderCode;
17+
18+
private OrderStatus orderStatus;
19+
20+
private PaymentStatus paymentStatus;
21+
22+
private String message;
23+
}

src/main/java/com/fisa/wonq/order/feign/pg/PgFeignClient.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import com.fisa.wonq.global.config.feign.PgClientConfig;
44
import com.fisa.wonq.order.feign.pg.dto.BaseResponse;
55
import com.fisa.wonq.order.feign.pg.dto.PaymentDto;
6+
import com.fisa.wonq.order.feign.pg.dto.RefundRequestDto;
7+
import com.fisa.wonq.order.feign.pg.dto.RefundResponseDto;
68
import org.springframework.cloud.openfeign.FeignClient;
79
import org.springframework.http.ResponseEntity;
810
import org.springframework.web.bind.annotation.PathVariable;
911
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
1013
import org.springframework.web.bind.annotation.RequestHeader;
1114

1215
@FeignClient(name = "pgClient", url = "${app.pg.endpoint}", configuration = PgClientConfig.class)
@@ -22,4 +25,15 @@ public interface PgFeignClient {
2225
ResponseEntity<BaseResponse<PaymentDto>> getPaymentByOrderCode(
2326
@PathVariable("orderCode") String orderCode
2427
);
28+
29+
/**
30+
* 결제 환불 API
31+
*
32+
* @param dto 환불 요청 DTO
33+
* @return 환불 처리 응답
34+
*/
35+
@PostMapping("/api/payments/refund")
36+
ResponseEntity<BaseResponse<RefundResponseDto>> refundPayment(
37+
@RequestBody RefundRequestDto dto
38+
);
2539
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.fisa.wonq.order.feign.pg.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
/**
8+
* PG사로 환불 요청 DTO
9+
*
10+
* 원큐오더 클라이언트로부터 환불 요청을 받는 DTO와 유사하지만,
11+
* 결제 ID만 포함되어 있습니다.
12+
*/
13+
@Getter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class RefundRequestDto {
17+
18+
private String paymentId;
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.fisa.wonq.order.feign.pg.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
/**
8+
* PG사로 환불 응답 DTO
9+
*
10+
* 원큐오더 클라이언트로부터 환불 요청을 받는 DTO와 유사하지만,
11+
* 결제 ID와 트랜잭션 ID, 결제 상태를 포함합니다.
12+
*/
13+
@Getter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class RefundResponseDto {
17+
18+
private Long paymentId;
19+
20+
private String txnId;
21+
22+
private PaymentStatus paymentStatus;
23+
}

0 commit comments

Comments
 (0)