POST /ai/admin/finance/card-refund/{transactionId}
캔디페이 카드 결제 취소 — 결제 전액을 카드로 돌려주고 기업 글로벌 크레딧에서 차감 (어드민)
카드 결제 취소 (전액 환불)
캔디페이로 결제한 크레딧 거래 하나를 골라 캔디페이 취소 API 를 호출하고, 기업 글로벌 크레딧(Business.remainCredit)에서 결제 금액 전액을 차감(REFUND 행)합니다. 결제 자체를 없던 일로 만드는 액션입니다. 카드·계좌이체 등 캔디페이 안의 결제 수단과 무관하게 같은 API 로 취소됩니다.
같은 행이 화면 API 마다 다른 이름으로 내려옵니다. 어느 쪽이든 취소 대상입니다.
| 어디서 | 타입 필드 | id | 이미 취소됨 |
|---|---|---|---|
재무 타임라인 (GET /ai/admin/finance/business/{businessAccountId} 의 globalTimeline, GET .../collab/{collabNo} 의 timeline) | type === "CARD_PAYMENT" | sourceKey 가 creditTx:1234 → 1234 | paymentCancelled === true |
원장 행 그대로 받는 곳 (CreditTransaction 원본) | transactionType === "PAYMENT" 이고 candyPaymentId 있음 | id | 같은 candyPaymentId 의 REFUND 행 존재 |
원장 타입 PAYMENT 에 candyPaymentId 가 없으면 타임라인은 CREDIT_ADD 로 내려주며, 그 행은 취소 대상이 아닙니다(409).
기업 셀프 환불(POST /ai/payments/credit/{transactionId}/cancel)과 달리 캠페인 진행 단계를 보지 않습니다. 관리자는 언제든 취소할 수 있어야 하기 때문입니다. 대신 "돈이 지금 어디 있는지"만 봅니다.
카드 환불은 기업 글로벌 크레딧에서만 빠집니다. 크레딧이 캠페인 예산에 예치된 상태(모집 시작 이후)면 글로벌 잔액이 부족해 409 로 막힙니다. 먼저 캠페인 강제 종료(waiveForfeit=true 면 전액)로 크레딧을 글로벌로 되돌린 뒤 호출하세요.
운영 순서
| 상황 | 순서 |
|---|---|
| 캠페인 시작 전 (크레딧이 지갑에 그대로) | 본 API 1회 |
| 캠페인 진행 중, 착수금은 뗄 때 | 강제 종료 → 본 API |
| 캠페인 진행 중, 전액 돌려줄 때 | 강제 종료(waiveForfeit=true) → 본 API |
| 잔액이 모자라도 무조건 돌려줘야 할 때 | 본 API + force=true (기업 잔액 마이너스) |
HTTP 요청
POST /ai/admin/finance/card-refund/{transactionId}
Authorization: Bearer {access_token}
Content-Type: application/jsonPath Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
transactionId | Long | 예 | 취소할 캔디페이 결제 행의 크레딧 거래 id (TB_CREDIT_TRANSACTION.id). 타임라인 CARD_PAYMENT 행의 sourceKey 숫자 = 원장 PAYMENT 행의 id |
Request Body (선택)
{
"reason": "고객 요청 — 캠페인 진행 불가",
"force": false
}| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
reason | String | 아니오 | 취소 사유. 캔디페이 취소 요청과 원장 memo 에 남습니다. 생략 시 관리자 결제 취소 |
force | boolean | 아니오 | 기업 글로벌 잔액이 환불 금액보다 적어도 강행. 잔액이 마이너스가 되며 슬랙에 경고가 붙습니다. 기본 false |
부분 환불은 지원하지 않습니다. 금액은 항상 원 결제 행의 amount 전액입니다.
동작
- 거래 조회 →
PAYMENT타입, 미삭제,candyPaymentId존재 확인 CandyPayment조회 → 이미CANCELLED면 거부, 같은 주문번호에 활성REFUND행이 있으면 거부- 기업 잔액
FOR UPDATE→remainCredit < amount이고force=false면409 - 캔디페이
POST /px/intents/{intentKey}/cancel호출 → 실패 시502, 원장 무변경 CandyPayment.status = CANCELLEDremainCredit -= amount,CreditTransaction(transactionType=REFUND, amount=-amount, collabNo, candyPaymentId, memo=reason, createdBy=adminId)저장- 커밋 후 결제·정산 알리미 슬랙 발송
캠페인 campaignSubStep 은 건드리지 않습니다 (기업 셀프 환불은 CAMPAIGN_PAYMENT 로 되돌리지만, 관리자 취소는 캠페인 상태와 분리).
응답 (200 OK)
{
"status": 200,
"code": null,
"message": "결제가 취소되었습니다.",
"data": {
"paymentTransactionId": 1234,
"refundTransactionId": 1301,
"candyPaymentId": "glowb_20260901_abcd",
"intentKey": "intent_xxxxxxxx",
"businessAccountId": 57,
"collabNo": 4321,
"refundAmount": 1000000,
"remainCreditBefore": 1000000,
"remainCreditAfter": 0,
"forced": false
}
}| 필드 | 타입 | 설명 |
|---|---|---|
paymentTransactionId | Long | 취소한 결제 행(원장 PAYMENT / 타임라인 CARD_PAYMENT) id |
refundTransactionId | Long | 새로 기록된 REFUND 행 id |
candyPaymentId | String | 캔디페이 주문번호 |
intentKey | String | 캔디페이 intentKey |
refundAmount | Integer | 환불 금액 (원 결제 전액) |
remainCreditBefore / remainCreditAfter | Integer | 기업 글로벌 잔액 변화 |
forced | boolean | 잔액 부족을 무시하고 강행했는지 |
에러 응답
| 상태 코드 | code | 사례 |
|---|---|---|
404 | NOT_FOUND | 거래·캔디페이 결제·기업 없음 |
409 | FINANCE_002 | 원장 PAYMENT 타입이 아님 / 삭제된 행 / candyPaymentId 없는 행(세금계산서 입금 등 캔디페이 안 거친 충전) / 금액 0 이하 |
400 | ALREADY_CANCELED_PAYMENT | 캔디페이 결제가 이미 취소됨 |
400 | ALREADY_REFUND_PAYMENT | 같은 주문번호에 REFUND 행이 이미 있음 |
409 | FINANCE_001 | 글로벌 잔액 부족 (force=false). 메시지에 캠페인 예치 금액과 안내 포함 |
502 | FINANCE_003 | 캔디페이 취소 API 실패. 메시지에 캔디페이 응답 코드 포함. 원장 무변경 |
401 | — | 인증 실패 |
캔디페이 취소가 성공한 뒤 DB 커밋이 실패하면 카드만 환불된 상태가 됩니다. 이 경우 [ADMIN_CARD_REFUND] 캔디페이 취소는 성공했으나 원장 기록 실패 ERROR 로그에 intentKey 가 남으니 원장에 REFUND 행을 수동으로 기입해 정합을 맞춥니다.