supplier/amadeus/
├── application/
│ ├── AmadeusRefundService.kt # 환불 서비스 메인 로직
│ └── AmadeusRetrieveService.kt # PNR/티켓 조회 서비스
├── infrastructure/
│ └── AmadeusClient.kt # SOAP API 클라이언트
├── domain/repository/
│ └── AmadeusInitRefundRepository.kt # InitRefund 캐싱 저장소
└── support/
├── model/
│ ├── WaiverModel.kt # Waiver 모델
│ ├── InitRefund.kt # 초기 환불 정보
│ └── Refund.kt # 최종 환불 결과
└── enums/
└── WaiverType.kt # Waiver 타입 (OSI/SSR/AUTH_CODE/REMARK)
2. 핵심 비즈니스 로직
2.1 환불 프로세스 플로우
flowchart TD
A[환불 요청] --> B{조회 or 실행?}
B -->|조회| C[refundCalculate]
B -->|실행| D[refund]
C --> E[PNR 정보 조회]
E --> F[티켓 문서 조회]
F --> G[병렬 initRefund 호출]
G --> H[환불 금액 계산]
H --> I[환불 예상 정보 반환]
D --> J[PNR 정보 조회]
J --> K[티켓 문서 조회]
K --> L{Waiver 존재?}
L -->|Yes| M[Waiver 저장]
L -->|No| N[티켓별 환불 처리]
M --> N
N --> O[initRefund]
O --> P{수정 필요?}
P -->|Yes| Q[updateRefund]
P -->|No| R[processRefund]
Q --> R
R --> S[환불 상태 검증]
S --> T[완료]
style L fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
style P fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
style S fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
2.2 환불 금액 계산 (refundCalculate)
2.2.1 조회 전용 로직
// AmadeusRefundService.kt:32-88fun refundCalculate(pnr: String, isWaiverRefund: Boolean): List<Refund> { return stateful { try { // 1. PNR 정보 조회 및 검증 val pnrInfo = start { amadeusClient.getPnrInfo(pnr = pnr, validateScheduleStatus = true, statefulBuilder = this) .also { validateRefundableStatus(pnrInfo = it, isWaiverRefund = isWaiverRefund) } } // 2. 티켓 문서 조회 및 검증 val tickets = inSeries { retrieveService.getPnrTicketDocuments(...) .also { validateRefundableTickets(pnr = pnr, tickets = it) } } // 3. 세션 종료 end { amadeusClient.signOut(statefulBuilder = this) } // 4. 병렬 initRefund 호출 (캐싱 활용) val initRefunds = withBlocking(Dispatchers.IO) { tickets.associateBy { it.passengerType }.toList().pmap { (passengerType, ticket) -> val ticketNumber = ticket.ticketNumber val key = "$pnr$ticketNumber" passengerType to (amadeusInitRefundRepository.find(key = key) ?: amadeusClient.initRefund(ticketNumber).also { // 당일 자정까지 캐싱 amadeusInitRefundRepository.save(key = key, value = it, ttl = ...) }) } }.getOrThrow() // 5. 환불 정보 반환 tickets.map { ticket -> val initRefund = initRefunds.first { (passengerType, _) -> passengerType == ticket.passengerType }.second Refund.of(ticket = ticket, initRefund = initRefund, isWaiverRefund = isWaiverRefund) } } catch (e: Exception) { // 세션 정리 if (session?.transactionStatusCode == TransactionStatusCode.InSeries) { end { amadeusClient.signOut(statefulBuilder = this) } } throw e } }}
2.2.2 캐싱 전략
캐시 키: {pnr}{ticketNumber}
TTL: 당일 자정까지
저장소: Redis (AmadeusInitRefundRepository)
목적: 동일 티켓에 대한 반복 조회 방지
2.3 실제 환불 처리 (refund)
2.3.1 메인 환불 로직
// AmadeusRefundService.kt:90-165fun refund(pnr: String, waivers: List<WaiverModel>?): List<Refund> { return stateful { val isWaiverRefund = !waivers.isNullOrEmpty() try { val pnrInfo = start { ... } val ticketDocuments = inSeries { ... } // Waiver 처리 if (isWaiverRefund) saveWaiverRefunds(waivers = waivers, pnrInfo = pnrInfo) // 티켓별 환불 처리 val refundedTickets = mutableListOf<String>() try { ticketDocuments.map { ticketDocument -> refund( validatingCarrier = pnrInfo.validatingCarrier ?: pnrInfo.schedules!!.first().marketingCarrier, ticket = ticketDocument, isWaiverRefund = isWaiverRefund, waivers = waivers, statefulBuilder = this ).also { refundedTickets.add(it.ticketNumber) } } } catch (e: Exception) { // 부분 실패 시 Slack 알림 slackService.sendAllTicketRefundFail(...) throw InternationalAdapterException(ErrorMessage.REFUND_FAILED, "All ticket is not refunded") }.also { // 환불 상태 검증 inSeries { retrieveService.getPnrTicketDocuments(...) .filter { it.status != TicketStatus.REFUND } .takeIf { it.isNotEmpty() } ?.run { throw InternationalAdapterException(ErrorMessage.REFUND_FAILED, ...) } } end { amadeusClient.signOut(statefulBuilder = this) } } } catch (e: Exception) { if (session?.transactionStatusCode == TransactionStatusCode.InSeries) { end { amadeusClient.signOut(statefulBuilder = this) } } throw e } }}
3. Waiver 처리
3.1 Waiver 타입
// WaiverType.kt:3-8enum class WaiverType { OSI, // Other Service Information SSR, // Special Service Request AUTH_CODE, // Authorization Code (환불 승인 코드) REMARK, // Remark}
3.2 Waiver 저장 및 검증
// AmadeusRefundService.kt:167-200private fun StatefulBuilder.saveWaiverRefunds(waivers: List<WaiverModel>, pnrInfo: PnrInfo) { val filteredWaivers = waivers.filter { it.type != WaiverType.AUTH_CODE } // AUTH_CODE는 별도 처리 if (filteredWaivers.isNotEmpty()) { // 1. Waiver 정보 저장 inSeries { amadeusClient.saveWaiverRefunds( waivers = filteredWaivers, validatingCarrier = pnrInfo.validatingCarrier, statefulBuilder = this ) } // 2. 저장 결과 조회 inSeries { amadeusClient.savePnrWithRetrieve(statefulBuilder = this) }.also { savedPnrInfo -> // 3. 저장 검증 filteredWaivers.forEach { waiver -> val elements = when (waiver.type) { WaiverType.OSI -> savedPnrInfo.reference.osiElements WaiverType.SSR -> savedPnrInfo.reference.ssrElements WaiverType.REMARK -> savedPnrInfo.reference.remarkElements else -> emptyList() } if (elements.none { it.texts?.contains(waiver.text) == true }) { throw InternationalAdapterException( ErrorMessage.REFUND_FAILED, "Not Found ${waiver.type} waiver code: ${waiver.text}", pnrInfo.pnr ?: "", pnrInfo.validatingCarrier ?: "" ) } } } }}
3.3 Waiver 처리 흐름
flowchart TD
A[Waiver 존재?] --> B{AUTH_CODE 제외 필터링}
B --> C[Waiver 저장 요청]
C --> D[PNR 저장 및 조회]
D --> E{타입별 검증}
E -->|OSI| F[osiElements 확인]
E -->|SSR| G[ssrElements 확인]
E -->|REMARK| H[remarkElements 확인]
F --> I{텍스트 포함?}
G --> I
H --> I
I -->|No| J[예외 발생]
I -->|Yes| K[검증 완료]
style E fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
style I fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
// AmadeusRefundService.kt:271-278val refundFee = if (isWaiverRefund) BigDecimal.ZERO else initRefund.refundFeeval noneRefundAmount = initRefund.usedAirPrice + initRefund.usedTax + refundFeeval (overrideCardAmount, overrideCashAmount) = if (cardAmount > noneRefundAmount) { // 카드 금액으로 수수료 충분히 커버 가능 cardAmount - noneRefundAmount to cashAmount} else { // 카드 금액 부족, 현금에서 추가 차감 BigDecimal.ZERO to cashAmount - (noneRefundAmount - cardAmount)}
4.2.3 환불 금액 계산 플로우
flowchart TD
A[원본 결제 금액] --> B{결제 수단 개수}
B -->|2개| C[카드 + 현금]
B -->|1개 카드| D[카드 + noneRefundAmount]
B -->|1개 현금| E[현금 + noneRefundAmount]
C --> F[환불 수수료 계산]
D --> F
E --> F
F --> G{Waiver?}
G -->|Yes| H[수수료 = 0]
G -->|No| I[수수료 = refundFee]
H --> J[차감할 금액 계산]
I --> J
J --> K[usedAirPrice + usedTax + 수수료]
K --> L{카드 금액 충분?}
L -->|Yes| M[카드에서만 차감]
L -->|No| N[카드 + 현금에서 차감]
M --> O[FOP 그룹 생성]
N --> O
style G fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
style L fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
sequenceDiagram
participant S as RefundService
participant AC as AmadeusClient
participant A as Amadeus API
S->>AC: initRefund(ticketNumber)
AC->>A: DocRefund_InitRefund
A-->>AC: InitRefund 정보 반환
AC-->>S: InitRefund
alt 수정 필요
S->>S: 환불 금액 계산
S->>S: FOP 그룹 재구성
S->>AC: updateRefund(initRefund, overrideCancelFee, overrideFopGroups, waiverCode)
AC->>A: DocRefund_UpdateRefund
A-->>AC: 업데이트 완료
AC-->>S: UpdateRefundReply
end
S->>AC: processRefund(validatingCarrier)
AC->>A: DocRefund_ProcessRefund
A-->>AC: 환불 처리 완료
AC-->>S: 완료
S->>AC: getPnrTicketDocuments (검증)
AC->>A: Ticket_DisplayTST
A-->>AC: 티켓 상태
AC-->>S: 환불 상태 확인