feat: P6 외부연동 어댑터 모듈 PoC (펌뱅킹/SMS·카카오)
신규 모듈 ga-external-adapter — 외부 SDK 의존성을 격리. 실제 SDK 통합 시 이 모듈의 구현체만 교체하면 ga-api/ga-common 무변경. - ga-common.adapter.bank: BankTransferAdapter 인터페이스 + Request/Response/Status - ga-common.adapter.message: MessageAdapter 인터페이스 + Channel(SMS/KAKAO)/Request/Response - ga-external-adapter: MockBankTransferAdapter / MockMessageAdapter (로그 + 마스킹) - V82: withdraw_request.transfer_tx_id/transfer_status/transfer_at + 부분 인덱스 - ApprovalService.handleApprovalCallback(WITHDRAW) — 최종 승인 시 펌뱅킹 어댑터 호출 - 성공: status APPROVED → SENT 전이, transfer_tx_id 영속화 - 실패: APPROVED 유지 + fail_reason 기록 (재시도 배치는 추후) - ApprovalService.sendStepNotification — DB 알림 외 SMS 어댑터 호출 (non-critical) - WithdrawRequestMapper.updateTransferResult 추가
This commit is contained in:
@@ -10,5 +10,6 @@ jar.enabled = false
|
||||
|
||||
dependencies {
|
||||
implementation project(':ga-core')
|
||||
implementation project(':ga-external-adapter')
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.ga.api.service.approval;
|
||||
|
||||
import com.ga.common.adapter.bank.BankTransferAdapter;
|
||||
import com.ga.common.adapter.bank.BankTransferRequest;
|
||||
import com.ga.common.adapter.bank.BankTransferResponse;
|
||||
import com.ga.common.adapter.bank.BankTransferStatus;
|
||||
import com.ga.common.adapter.message.MessageAdapter;
|
||||
import com.ga.common.adapter.message.MessageChannel;
|
||||
import com.ga.common.adapter.message.MessageRequest;
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
@@ -12,6 +19,7 @@ import com.ga.core.mapper.settle.SettleMasterMapper;
|
||||
import com.ga.core.mapper.withdraw.WithdrawRequestMapper;
|
||||
import com.ga.core.vo.approval.*;
|
||||
import com.ga.core.vo.notice.NotificationRefType;
|
||||
import com.ga.core.vo.withdraw.WithdrawRequestVO;
|
||||
// Note: ApprovalLineVO kept for createLine/updateLine methods
|
||||
import com.ga.core.vo.notice.UserNotificationVO;
|
||||
import com.ga.core.vo.settle.SettleStatus;
|
||||
@@ -41,6 +49,10 @@ public class ApprovalService {
|
||||
private final CommissionDisputeMapper disputeMapper;
|
||||
private final IncentivePaymentMapper incentivePaymentMapper;
|
||||
|
||||
// P6 외부연동 어댑터 — 실제 구현은 ga-external-adapter 모듈 (현재 Mock)
|
||||
private final BankTransferAdapter bankTransferAdapter;
|
||||
private final MessageAdapter messageAdapter;
|
||||
|
||||
// ── Line ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public PageResponse<ApprovalLineResp> listLines(ApprovalLineSearchParam param) {
|
||||
@@ -228,6 +240,7 @@ public class ApprovalService {
|
||||
case "WITHDRAW" -> {
|
||||
withdrawMapper.updateStatus(targetId, "APPROVED", null);
|
||||
log.info("출금신청 승인 완료: requestId={}", targetId);
|
||||
requestBankTransfer(targetId);
|
||||
}
|
||||
case "SETTLE" -> {
|
||||
settleMasterMapper.updateStatus(targetId, SettleStatus.CONFIRMED.name(),
|
||||
@@ -252,7 +265,7 @@ public class ApprovalService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 다음 결재 단계 담당자에게 알림 발송 */
|
||||
/** 다음 결재 단계 담당자에게 알림 발송 (DB + 외부채널) */
|
||||
private void sendStepNotification(Long requestId, Long lineId, int stepNo, String targetType) {
|
||||
try {
|
||||
ApprovalLineStepVO step = stepMapper.selectByLineAndStep(lineId, stepNo);
|
||||
@@ -260,14 +273,79 @@ public class ApprovalService {
|
||||
// roleCode 기반 userId 매핑은 P5에서 고도화 — 현재는 메타만 저장
|
||||
UserNotificationVO noti = new UserNotificationVO();
|
||||
noti.setUserId(0L); // 실제 담당자 ID 조회 로직은 P5
|
||||
noti.setTitle("[결재요청] " + (targetType != null ? targetType : "") + " 결재 " + stepNo + "단계");
|
||||
noti.setContent("결재 요청 ID " + requestId + " 에 대한 " + stepNo + "단계 결재가 필요합니다.");
|
||||
String title = "[결재요청] " + (targetType != null ? targetType : "") + " 결재 " + stepNo + "단계";
|
||||
String content = "결재 요청 ID " + requestId + " 에 대한 " + stepNo + "단계 결재가 필요합니다.";
|
||||
noti.setTitle(title);
|
||||
noti.setContent(content);
|
||||
noti.setRefType(NotificationRefType.APPROVAL.name());
|
||||
noti.setRefId(requestId);
|
||||
noti.setIsRead(false);
|
||||
notificationMapper.insert(noti);
|
||||
|
||||
// SMS/카카오 외부채널 — Mock 단계라 항상 시도. 실패해도 DB 알림은 살아있다.
|
||||
sendExternalMessage(MessageChannel.SMS, title, content, requestId);
|
||||
} catch (Exception e) {
|
||||
log.warn("결재 알림 발송 실패 (non-critical): requestId={}", requestId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출금 결재 최종 승인 → 펌뱅킹 어댑터 요청.
|
||||
* <p>
|
||||
* 어댑터가 실패 응답을 줘도 결재 자체는 이미 APPROVED 이므로 예외를 던지지 않는다.
|
||||
* (재시도 배치는 transfer_status='FAILED' 필터로 별도 구현 예정.)
|
||||
*/
|
||||
private void requestBankTransfer(Long withdrawId) {
|
||||
try {
|
||||
WithdrawRequestVO vo = withdrawMapper.selectVOById(withdrawId);
|
||||
if (vo == null) {
|
||||
log.warn("[BANK] withdraw 조회 실패: requestId={}", withdrawId);
|
||||
return;
|
||||
}
|
||||
BankTransferRequest req = BankTransferRequest.builder()
|
||||
.withdrawRequestId(withdrawId)
|
||||
.bankCode(vo.getBankCode())
|
||||
.accountNo(vo.getAccountNo()) // 복호화 평문
|
||||
.amount(vo.getAmount())
|
||||
.memo("GA수수료 출금 #" + withdrawId)
|
||||
.build();
|
||||
BankTransferResponse resp = bankTransferAdapter.requestTransfer(req);
|
||||
|
||||
boolean ok = resp != null && resp.getStatus() != BankTransferStatus.FAILED;
|
||||
String nextStatus = ok ? "SENT" : "APPROVED";
|
||||
String failReason = ok ? null : (resp != null ? resp.getResultMessage() : "no response");
|
||||
withdrawMapper.updateTransferResult(
|
||||
withdrawId,
|
||||
resp != null ? resp.getTxId() : null,
|
||||
resp != null && resp.getStatus() != null ? resp.getStatus().name() : null,
|
||||
nextStatus,
|
||||
failReason
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// 어댑터 자체가 예외를 던진 경우 — APPROVED 상태로 두고 fail_reason 만 갱신.
|
||||
log.error("[BANK] 펌뱅킹 어댑터 호출 실패: requestId={}", withdrawId, e);
|
||||
try {
|
||||
withdrawMapper.updateTransferResult(
|
||||
withdrawId, null, BankTransferStatus.FAILED.name(),
|
||||
"APPROVED", e.getMessage());
|
||||
} catch (Exception ignore) { /* 결재 흐름 깨지지 않게 swallow */ }
|
||||
}
|
||||
}
|
||||
|
||||
/** SMS/카카오 어댑터로 메시지 전송 — Mock 단계라 PoC 용 phone "010-0000-0000" 고정. */
|
||||
private void sendExternalMessage(MessageChannel channel, String title, String content, Long refId) {
|
||||
try {
|
||||
MessageRequest req = MessageRequest.builder()
|
||||
.channel(channel)
|
||||
.phoneNumber("010-0000-0000") // P6 보강: user.phone 조회로 교체
|
||||
.title(title)
|
||||
.content(content)
|
||||
.refType(NotificationRefType.APPROVAL.name())
|
||||
.refId(refId)
|
||||
.build();
|
||||
messageAdapter.send(req);
|
||||
} catch (Exception e) {
|
||||
log.warn("[MSG] {} 발송 실패 (non-critical): refId={}", channel, refId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user