feat(api): TaxCalculator 도메인 + PaymentService 세금 통합 + AgentService 세무 매핑 (도메인 P0-1-c)
P0-1 세무 처리 마무리. ga-batch 정산 step 통합은 도메인 P0 마무리 단계.
TaxCalculator (신규, ga-api/service/tax):
- @Component pure function: TaxBreakdown calculate(amount, taxType, vatType)
- record TaxBreakdown {incomeTax, localTax, vat, netAmount}
- 세율은 SystemConfigService로 외부화 (TAX_RATE_BUSINESS_INCOME 등)
- 공식:
- incomeTax = amount × incomeRate / 100 (ROUND_HALF_UP, scale 0)
- localTax = incomeTax × localRate / 100
- vat = (TAXABLE) ? amount × vatRate / 100 : 0
- netAmount = amount - incomeTax - localTax + vat
PaymentService:
- calculateTaxes(paymentId) — 단건 재계산 @Transactional
- calculateTaxesForMonth(settleMonth) — 월별 일괄 @Transactional + batch update
PaymentMapper (ga-core):
- updateTaxes / updateTaxesBatch / selectBySettleMonth 추가
AgentService.toVO():
- taxType/vatType/businessNo 매핑 + default 처리
- 사업소득자인데 businessNo 미입력 시 경고 로그
PaymentController:
- POST /api/payments/{id}/calculate-tax (@RequirePermission, @DataChangeLog)
- POST /api/payments/calculate-taxes?settleMonth=YYYYMM
This commit is contained in:
@@ -48,4 +48,23 @@ public class PaymentController {
|
||||
@RequestParam String bankCode) {
|
||||
return ApiResponse.ok(paymentService.generateTransferFile(settleMonth, bankCode));
|
||||
}
|
||||
|
||||
@Operation(summary = "세금 단건 재계산")
|
||||
@PostMapping("/{paymentId}/calculate-tax")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
public ApiResponse<Void> calculateTax(@PathVariable Long paymentId) {
|
||||
paymentService.calculateTaxes(paymentId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "세금 월별 일괄 재계산")
|
||||
@PostMapping("/calculate-taxes")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
public ApiResponse<Void> calculateTaxesForMonth(
|
||||
@Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) {
|
||||
paymentService.calculateTaxesForMonth(settleMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.org.AgentMapper;
|
||||
import com.ga.core.vo.org.*;
|
||||
import com.ga.core.vo.org.AgentStatus;
|
||||
import com.ga.core.vo.org.TaxType;
|
||||
import com.ga.core.vo.org.VatType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.cursor.Cursor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentService {
|
||||
@@ -102,6 +106,17 @@ public class AgentService {
|
||||
vo.setBankCode(req.getBankCode());
|
||||
vo.setAccountNo(req.getAccountNo());
|
||||
vo.setJoinDate(req.getJoinDate());
|
||||
|
||||
String taxType = req.getTaxType() != null ? req.getTaxType() : TaxType.BUSINESS_INCOME.name();
|
||||
String vatType = req.getVatType() != null ? req.getVatType() : VatType.NONE.name();
|
||||
vo.setTaxType(taxType);
|
||||
vo.setVatType(vatType);
|
||||
vo.setBusinessNo(req.getBusinessNo());
|
||||
|
||||
if (TaxType.BUSINESS_INCOME.name().equals(taxType)
|
||||
&& (req.getBusinessNo() == null || req.getBusinessNo().isBlank())) {
|
||||
log.warn("agentId={} taxType=BUSINESS_INCOME이지만 businessNo가 비어있습니다", agentId);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package com.ga.api.service.settle;
|
||||
|
||||
import com.ga.api.service.tax.TaxBreakdown;
|
||||
import com.ga.api.service.tax.TaxCalculator;
|
||||
import com.ga.api.service.transfer.TransferFileService;
|
||||
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.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.PaymentResp;
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
import com.ga.core.vo.settle.SettleSearchParam;
|
||||
import com.ga.core.vo.settle.TransferFileResp;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentService {
|
||||
|
||||
private final PaymentMapper mapper;
|
||||
private final AgentMapper agentMapper;
|
||||
private final TransferFileService transferFileService;
|
||||
private final TaxCalculator taxCalculator;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<PaymentResp> list(SettleSearchParam param) {
|
||||
@@ -31,4 +46,55 @@ public class PaymentService {
|
||||
public TransferFileResp generateTransferFile(String settleMonth, String bankCode) {
|
||||
return transferFileService.generate(settleMonth, bankCode);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void calculateTaxes(Long paymentId) {
|
||||
PaymentVO payment = mapper.selectById(paymentId);
|
||||
if (payment == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
applyTaxes(payment);
|
||||
mapper.updateTaxes(payment.getPaymentId(),
|
||||
payment.getIncomeTaxAmount(), payment.getLocalTaxAmount(),
|
||||
payment.getVatAmount(), payment.getNetAmount());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void calculateTaxesForMonth(String settleMonth) {
|
||||
List<PaymentVO> payments = mapper.selectBySettleMonth(settleMonth);
|
||||
for (PaymentVO payment : payments) {
|
||||
applyTaxes(payment);
|
||||
}
|
||||
if (!payments.isEmpty()) {
|
||||
mapper.updateTaxesBatch(payments);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTaxes(PaymentVO payment) {
|
||||
AgentVO agent = agentMapper.selectById(payment.getAgentId());
|
||||
TaxType taxType = parseTaxType(agent);
|
||||
VatType vatType = parseVatType(agent);
|
||||
|
||||
if (taxType == TaxType.BUSINESS_INCOME && (agent == null || agent.getBusinessNo() == null || agent.getBusinessNo().isBlank())) {
|
||||
log.warn("agentId={} taxType=BUSINESS_INCOME이지만 businessNo가 비어있습니다", payment.getAgentId());
|
||||
}
|
||||
|
||||
TaxBreakdown breakdown = taxCalculator.calculate(payment.getPayAmount(), taxType, vatType);
|
||||
payment.setIncomeTaxAmount(breakdown.incomeTax());
|
||||
payment.setLocalTaxAmount(breakdown.localTax());
|
||||
payment.setVatAmount(breakdown.vat());
|
||||
payment.setNetAmount(breakdown.netAmount());
|
||||
}
|
||||
|
||||
private TaxType parseTaxType(AgentVO agent) {
|
||||
if (agent != null && agent.getTaxType() != null) {
|
||||
try { return TaxType.valueOf(agent.getTaxType()); } catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
return TaxType.BUSINESS_INCOME;
|
||||
}
|
||||
|
||||
private VatType parseVatType(AgentVO agent) {
|
||||
if (agent != null && agent.getVatType() != null) {
|
||||
try { return VatType.valueOf(agent.getVatType()); } catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
return VatType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ga.api.service.tax;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record TaxBreakdown(
|
||||
BigDecimal incomeTax,
|
||||
BigDecimal localTax,
|
||||
BigDecimal vat,
|
||||
BigDecimal netAmount
|
||||
) {}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ga.api.service.tax;
|
||||
|
||||
import com.ga.common.system.SystemConfigService;
|
||||
import com.ga.core.vo.org.TaxType;
|
||||
import com.ga.core.vo.org.VatType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TaxCalculator {
|
||||
|
||||
private final SystemConfigService configService;
|
||||
|
||||
public TaxBreakdown calculate(BigDecimal amount, TaxType taxType, VatType vatType) {
|
||||
BigDecimal incomeRate = resolveIncomeRate(taxType);
|
||||
BigDecimal localRate = configService.getDecimal("TAX_RATE_LOCAL_INCOME", new BigDecimal("10.0"));
|
||||
BigDecimal vatRate = configService.getDecimal("TAX_RATE_VAT", new BigDecimal("10.0"));
|
||||
|
||||
BigDecimal incomeTax = amount.multiply(incomeRate)
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||
BigDecimal localTax = incomeTax.multiply(localRate)
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||
BigDecimal vat = VatType.TAXABLE.equals(vatType)
|
||||
? amount.multiply(vatRate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
BigDecimal netAmount = amount.subtract(incomeTax).subtract(localTax).add(vat);
|
||||
|
||||
return new TaxBreakdown(incomeTax, localTax, vat, netAmount);
|
||||
}
|
||||
|
||||
private BigDecimal resolveIncomeRate(TaxType taxType) {
|
||||
if (TaxType.OTHER_INCOME.equals(taxType)) {
|
||||
return configService.getDecimal("TAX_RATE_OTHER_INCOME", new BigDecimal("8.8"));
|
||||
}
|
||||
return configService.getDecimal("TAX_RATE_BUSINESS_INCOME", new BigDecimal("3.3"));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.ga.core.vo.settle.SettleSearchParam;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
@@ -19,7 +20,14 @@ public interface PaymentMapper {
|
||||
@Param("failReason") String failReason);
|
||||
int updateTransferFile(@Param("paymentIds") List<Long> paymentIds,
|
||||
@Param("transferFileId") Long transferFileId);
|
||||
int updateTaxes(@Param("paymentId") Long paymentId,
|
||||
@Param("incomeTaxAmount") BigDecimal incomeTaxAmount,
|
||||
@Param("localTaxAmount") BigDecimal localTaxAmount,
|
||||
@Param("vatAmount") BigDecimal vatAmount,
|
||||
@Param("netAmount") BigDecimal netAmount);
|
||||
int updateTaxesBatch(@Param("list") List<PaymentVO> list);
|
||||
|
||||
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