feat(api): DeductionService + PaymentService 차감 통합 + DeductionController (도메인 P0-2-c)
P0-2 공제/차감 도메인 마무리. ga-batch 정산 step 통합은 도메인 P0 마무리에서.
DeductionService (신규):
- CRUD: list/detail/create/update/cancel
- create는 (agent_id, settle_month, deduction_type) 유니크 중복 검증
- cancel은 status를 CANCELLED로 전이
- 정산용: sumPendingByAgent, selectPendingByMonth, markAsApplied(batch)
PaymentService 확장:
- applyDeductions(paymentId, settleMonth) — 단건:
1. PENDING 차감 조회
2. payment_deduction_detail INSERT
3. agent_deduction.status → APPLIED 일괄 전이
4. payment.deduction_amount UPDATE
5. net_amount 재계산 (newNet = currentNet - deductionTotal)
- applyDeductionsForMonth(settleMonth) — 월 일괄
net_amount 공식 갱신:
- 세금 적용(calculateTaxes): amount - incomeTax - localTax + vat
- 차감 적용(applyDeductions): currentNet - deductionTotal
- 최종: amount - incomeTax - localTax + vat - deduction
Mapper 추가 (ga-core):
- AgentDeductionMapper: selectPendingByAgent, updateStatusBatch, countByAgentMonthType
- PaymentMapper: updateDeduction
DeductionController + PaymentController 엔드포인트 추가:
- /api/deductions CRUD (5종)
- /api/payments/{id}/apply-deductions
- /api/payments/apply-deductions?settleMonth=YYYYMM
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.controller.deduction;
|
||||
|
||||
import com.ga.api.service.deduction.DeductionService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.settle.AgentDeductionResp;
|
||||
import com.ga.core.vo.settle.AgentDeductionSaveReq;
|
||||
import com.ga.core.vo.settle.AgentDeductionSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "공제 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/deductions")
|
||||
@RequiredArgsConstructor
|
||||
public class DeductionController {
|
||||
|
||||
private final DeductionService deductionService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "READ")
|
||||
public ApiResponse<PageResponse<AgentDeductionResp>> list(@ModelAttribute AgentDeductionSearchParam param) {
|
||||
return ApiResponse.ok(deductionService.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{deductionId}")
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "READ")
|
||||
public ApiResponse<AgentDeductionResp> detail(@PathVariable Long deductionId) {
|
||||
return ApiResponse.ok(deductionService.detail(deductionId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "CREATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody AgentDeductionSaveReq req) {
|
||||
return ApiResponse.ok(deductionService.create(req));
|
||||
}
|
||||
|
||||
@PutMapping("/{deductionId}")
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
public ApiResponse<Void> update(@PathVariable Long deductionId,
|
||||
@Valid @RequestBody AgentDeductionSaveReq req) {
|
||||
deductionService.update(deductionId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{deductionId}/cancel")
|
||||
@RequirePermission(menu = "DEDUCTION", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "DEDUCTION", table = "agent_deduction")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long deductionId) {
|
||||
deductionService.cancel(deductionId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,25 @@ public class PaymentController {
|
||||
paymentService.calculateTaxesForMonth(settleMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "차감 단건 적용")
|
||||
@PostMapping("/{paymentId}/apply-deductions")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
public ApiResponse<Void> applyDeductions(
|
||||
@PathVariable Long paymentId,
|
||||
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||
paymentService.applyDeductions(paymentId, settleMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "차감 월별 일괄 적용")
|
||||
@PostMapping("/apply-deductions")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
public ApiResponse<Void> applyDeductionsForMonth(
|
||||
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||
paymentService.applyDeductionsForMonth(settleMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.ga.api.service.deduction;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.settle.AgentDeductionMapper;
|
||||
import com.ga.core.vo.settle.AgentDeductionResp;
|
||||
import com.ga.core.vo.settle.AgentDeductionSaveReq;
|
||||
import com.ga.core.vo.settle.AgentDeductionSearchParam;
|
||||
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||
import com.ga.core.vo.settle.DeductionStatus;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeductionService {
|
||||
|
||||
private final AgentDeductionMapper mapper;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<AgentDeductionResp> list(AgentDeductionSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public AgentDeductionResp detail(Long deductionId) {
|
||||
AgentDeductionResp resp = mapper.selectDetailById(deductionId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(AgentDeductionSaveReq req) {
|
||||
if (mapper.countByAgentMonthType(req.getAgentId(), req.getSettleMonth(), req.getDeductionType()) > 0) {
|
||||
throw new BizException(ErrorCode.DUPLICATE_DATA,
|
||||
"동일 설계사/정산월/차감유형 조합이 이미 존재합니다");
|
||||
}
|
||||
AgentDeductionVO vo = new AgentDeductionVO();
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setDeductionType(req.getDeductionType());
|
||||
vo.setAmount(req.getAmount());
|
||||
vo.setDescription(req.getDescription());
|
||||
vo.setStatus(DeductionStatus.PENDING.name());
|
||||
mapper.insert(vo);
|
||||
return vo.getDeductionId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Long deductionId, AgentDeductionSaveReq req) {
|
||||
AgentDeductionVO vo = mapper.selectById(deductionId);
|
||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!DeductionStatus.PENDING.name().equals(vo.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "PENDING 상태만 수정 가능합니다");
|
||||
}
|
||||
vo.setAmount(req.getAmount());
|
||||
vo.setDescription(req.getDescription());
|
||||
mapper.update(vo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancel(Long deductionId) {
|
||||
AgentDeductionVO vo = mapper.selectById(deductionId);
|
||||
if (vo == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (DeductionStatus.APPLIED.name().equals(vo.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "이미 적용된 차감은 취소할 수 없습니다");
|
||||
}
|
||||
mapper.updateStatus(deductionId, DeductionStatus.CANCELLED.name());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal sumPendingByAgent(Long agentId, String settleMonth) {
|
||||
BigDecimal sum = mapper.sumPendingByAgent(agentId, settleMonth);
|
||||
return sum != null ? sum : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AgentDeductionVO> selectPendingByMonth(String settleMonth) {
|
||||
return mapper.selectPendingByMonth(settleMonth);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AgentDeductionVO> selectPendingByAgent(Long agentId, String settleMonth) {
|
||||
return mapper.selectPendingByAgent(agentId, settleMonth);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsApplied(List<Long> deductionIds) {
|
||||
if (deductionIds.isEmpty()) return;
|
||||
mapper.updateStatusBatch(deductionIds, DeductionStatus.APPLIED.name());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ga.api.service.settle;
|
||||
|
||||
import com.ga.api.service.deduction.DeductionService;
|
||||
import com.ga.api.service.tax.TaxBreakdown;
|
||||
import com.ga.api.service.tax.TaxCalculator;
|
||||
import com.ga.api.service.transfer.TransferFileService;
|
||||
@@ -7,10 +8,13 @@ import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.org.AgentMapper;
|
||||
import com.ga.core.mapper.settle.PaymentDeductionDetailMapper;
|
||||
import com.ga.core.mapper.settle.PaymentMapper;
|
||||
import com.ga.core.vo.org.AgentVO;
|
||||
import com.ga.core.vo.org.TaxType;
|
||||
import com.ga.core.vo.org.VatType;
|
||||
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||
import com.ga.core.vo.settle.PaymentDeductionDetailVO;
|
||||
import com.ga.core.vo.settle.PaymentResp;
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
import com.ga.core.vo.settle.SettleSearchParam;
|
||||
@@ -20,7 +24,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -29,8 +36,10 @@ public class PaymentService {
|
||||
|
||||
private final PaymentMapper mapper;
|
||||
private final AgentMapper agentMapper;
|
||||
private final PaymentDeductionDetailMapper deductionDetailMapper;
|
||||
private final TransferFileService transferFileService;
|
||||
private final TaxCalculator taxCalculator;
|
||||
private final DeductionService deductionService;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<PaymentResp> list(SettleSearchParam param) {
|
||||
@@ -68,6 +77,48 @@ public class PaymentService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void applyDeductions(Long paymentId, String settleMonth) {
|
||||
PaymentVO payment = mapper.selectById(paymentId);
|
||||
if (payment == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
|
||||
List<AgentDeductionVO> pending = deductionService.selectPendingByAgent(payment.getAgentId(), settleMonth);
|
||||
if (pending.isEmpty()) return;
|
||||
|
||||
// 명세 기록
|
||||
List<PaymentDeductionDetailVO> details = pending.stream().map(d -> {
|
||||
PaymentDeductionDetailVO detail = new PaymentDeductionDetailVO();
|
||||
detail.setPaymentId(paymentId);
|
||||
detail.setDeductionType(d.getDeductionType());
|
||||
detail.setAmount(d.getAmount());
|
||||
detail.setDescription(d.getDescription());
|
||||
detail.setAppliedAt(LocalDateTime.now());
|
||||
return detail;
|
||||
}).collect(Collectors.toList());
|
||||
deductionDetailMapper.insertBatch(details);
|
||||
|
||||
// 상태 전이
|
||||
List<Long> ids = pending.stream().map(AgentDeductionVO::getDeductionId).collect(Collectors.toList());
|
||||
deductionService.markAsApplied(ids);
|
||||
|
||||
// deduction_amount + net_amount UPDATE
|
||||
// net_amount = amount − incomeTax − localTax + vat − deductionAmount
|
||||
BigDecimal deductionTotal = pending.stream()
|
||||
.map(AgentDeductionVO::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal currentNet = payment.getNetAmount() != null ? payment.getNetAmount() : payment.getPayAmount();
|
||||
BigDecimal newNet = currentNet.subtract(deductionTotal);
|
||||
mapper.updateDeduction(paymentId, deductionTotal, newNet);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void applyDeductionsForMonth(String settleMonth) {
|
||||
List<PaymentVO> payments = mapper.selectBySettleMonth(settleMonth);
|
||||
for (PaymentVO payment : payments) {
|
||||
applyDeductions(payment.getPaymentId(), settleMonth);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTaxes(PaymentVO payment) {
|
||||
AgentVO agent = agentMapper.selectById(payment.getAgentId());
|
||||
TaxType taxType = parseTaxType(agent);
|
||||
|
||||
@@ -25,4 +25,17 @@ public interface AgentDeductionMapper {
|
||||
|
||||
/** 정산월 전체 PENDING 목록 (배치용) */
|
||||
List<AgentDeductionVO> selectPendingByMonth(@Param("settleMonth") String settleMonth);
|
||||
|
||||
/** 설계사의 PENDING 목록 (단건 차감 적용용) */
|
||||
List<AgentDeductionVO> selectPendingByAgent(@Param("agentId") Long agentId,
|
||||
@Param("settleMonth") String settleMonth);
|
||||
|
||||
/** 일괄 상태 전이 */
|
||||
int updateStatusBatch(@Param("deductionIds") List<Long> deductionIds,
|
||||
@Param("status") String status);
|
||||
|
||||
/** 중복 검증 */
|
||||
int countByAgentMonthType(@Param("agentId") Long agentId,
|
||||
@Param("settleMonth") String settleMonth,
|
||||
@Param("deductionType") String deductionType);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ public interface PaymentMapper {
|
||||
@Param("netAmount") BigDecimal netAmount);
|
||||
int updateTaxesBatch(@Param("list") List<PaymentVO> list);
|
||||
|
||||
int updateDeduction(@Param("paymentId") Long paymentId,
|
||||
@Param("deductionAmount") BigDecimal deductionAmount,
|
||||
@Param("netAmount") BigDecimal netAmount);
|
||||
|
||||
List<PaymentVO> selectPendingByMonthAndBank(@Param("settleMonth") String settleMonth,
|
||||
@Param("bankCode") String bankCode);
|
||||
List<PaymentVO> selectBySettleMonth(@Param("settleMonth") String settleMonth);
|
||||
|
||||
Reference in New Issue
Block a user