feat: P6 후속 4 — 출금 폼 잔여 한도 미리 표시

- 백엔드 GET /api/mobile/me/settle-masters/{id}/withdrawable-balance
  · 응답 {settleId, netAmount, usedAmount, remainingAmount}
  · 본인 settle_master 검증 후 net - sumActiveAmountByMaster 반환
  · createWithdrawRequest 가 동일 조회 로직 재사용 (중복 제거)
- PWA WithdrawCreate: settleMaster 선택 시 잔액 카드 표시 + amount 클라이언트 사전 검증

검증: ga-api compileJava BUILD SUCCESSFUL / ga-mobile-pwa tsc --noEmit 0 오류
(라이브 e2e 는 DB/docker 미가동으로 차단 — 환경 복구 후 스모크 예정)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-29 20:51:31 +09:00
parent 366f49c3f0
commit d8d64625a9
4 changed files with 88 additions and 13 deletions
@@ -3,6 +3,7 @@ package com.ga.api.controller.mobile;
import com.ga.api.service.mobile.MobileMeService;
import com.ga.api.service.mobile.MobileMeService.MeSummary;
import com.ga.api.service.mobile.MobileMeService.MobileWithdrawSaveReq;
import com.ga.api.service.mobile.MobileMeService.WithdrawableBalance;
import com.ga.common.model.ApiResponse;
import com.ga.common.model.PageResponse;
import com.ga.core.vo.notice.UserNotificationResp;
@@ -69,6 +70,11 @@ public class MobileMeController {
return ApiResponse.ok(service.withdrawableSettleMasters());
}
@GetMapping("/settle-masters/{settleMasterId}/withdrawable-balance")
public ApiResponse<WithdrawableBalance> withdrawableBalance(@PathVariable Long settleMasterId) {
return ApiResponse.ok(service.withdrawableBalance(settleMasterId));
}
@GetMapping("/withdraw-requests")
public ApiResponse<PageResponse<WithdrawRequestResp>> withdrawRequests(
@ModelAttribute WithdrawRequestSearchParam param) {
@@ -168,6 +168,28 @@ public class MobileMeService {
withdrawRequestService.cancel(requestId);
}
/**
* 본인 정산 마스터의 출금 가능 잔액 조회 — 출금 신청 폼 미리 표시용.
* 본인 소유가 아니면 FORBIDDEN, 없으면 NOT_FOUND.
*/
@Transactional(readOnly = true)
public WithdrawableBalance withdrawableBalance(Long settleMasterId) {
Long agentId = requireAgentId();
var master = settleMasterMapper.selectById(settleMasterId);
if (master == null) throw new BizException(ErrorCode.NOT_FOUND, "정산 마스터를 찾을 수 없습니다");
if (!agentId.equals(master.getAgentId())) throw new BizException(ErrorCode.FORBIDDEN);
BigDecimal netAmount = master.getNetAmount() != null ? master.getNetAmount() : BigDecimal.ZERO;
BigDecimal used = withdrawMapper.sumActiveAmountByMaster(settleMasterId);
if (used == null) used = BigDecimal.ZERO;
return WithdrawableBalance.builder()
.settleId(settleMasterId)
.netAmount(netAmount)
.usedAmount(used)
.remainingAmount(netAmount.subtract(used))
.build();
}
/**
* 본인 출금 신청 등록. agentId / requestedBy 는 토큰에서 강제 결정.
* 클라이언트가 보낸 값은 무시한다.
@@ -175,22 +197,14 @@ public class MobileMeService {
@Transactional
public Long createWithdrawRequest(MobileWithdrawSaveReq req) {
Long userId = currentUserId();
Long agentId = requireAgentId();
// 1) settle_master 본인 소유 + net_amount 확보
var master = settleMasterMapper.selectById(req.getSettleMasterId());
if (master == null) throw new BizException(ErrorCode.NOT_FOUND, "정산 마스터를 찾을 수 없습니다");
if (!agentId.equals(master.getAgentId())) throw new BizException(ErrorCode.FORBIDDEN);
// 2) 잔여 한도 = net_amount - 활성 출금합계
BigDecimal netAmount = master.getNetAmount() != null ? master.getNetAmount() : BigDecimal.ZERO;
BigDecimal used = withdrawMapper.sumActiveAmountByMaster(req.getSettleMasterId());
if (used == null) used = BigDecimal.ZERO;
BigDecimal remaining = netAmount.subtract(used);
if (req.getAmount().compareTo(remaining) > 0) {
// 본인 소유 검증 + 잔여 한도 = net_amount - 활성 출금합계 (조회 로직 재사용)
WithdrawableBalance balance = withdrawableBalance(req.getSettleMasterId());
if (req.getAmount().compareTo(balance.getRemainingAmount()) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER,
"출금 가능 잔액(" + remaining + ")을 초과합니다");
"출금 가능 잔액(" + balance.getRemainingAmount() + ")을 초과합니다");
}
Long agentId = requireAgentId();
WithdrawRequestSaveReq inner = new WithdrawRequestSaveReq();
inner.setSettleMasterId(req.getSettleMasterId());
@@ -234,6 +248,16 @@ public class MobileMeService {
private int unreadNotificationCount;
}
/** 출금 가능 잔액 — 폼 미리 표시용. */
@Data
@Builder
public static class WithdrawableBalance {
private Long settleId;
private BigDecimal netAmount;
private BigDecimal usedAmount;
private BigDecimal remainingAmount;
}
/** 모바일 출금 신청 — 토큰에서 agentId/requestedBy 강제. */
@Data
public static class MobileWithdrawSaveReq {