diff --git a/ga-api/src/main/java/com/ga/api/controller/settle/PaymentController.java b/ga-api/src/main/java/com/ga/api/controller/settle/PaymentController.java index 044ffc9..c2cf63d 100644 --- a/ga-api/src/main/java/com/ga/api/controller/settle/PaymentController.java +++ b/ga-api/src/main/java/com/ga/api/controller/settle/PaymentController.java @@ -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 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 calculateTaxesForMonth( + @Pattern(regexp = "\\d{6}", message = "settleMonth는 YYYYMM 형식이어야 합니다") @RequestParam String settleMonth) { + paymentService.calculateTaxesForMonth(settleMonth); + return ApiResponse.ok(); + } } diff --git a/ga-api/src/main/java/com/ga/api/service/org/AgentService.java b/ga-api/src/main/java/com/ga/api/service/org/AgentService.java index 90ba600..e16e679 100644 --- a/ga-api/src/main/java/com/ga/api/service/org/AgentService.java +++ b/ga-api/src/main/java/com/ga/api/service/org/AgentService.java @@ -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; } } diff --git a/ga-api/src/main/java/com/ga/api/service/settle/PaymentService.java b/ga-api/src/main/java/com/ga/api/service/settle/PaymentService.java index 178db47..be6f3e5 100644 --- a/ga-api/src/main/java/com/ga/api/service/settle/PaymentService.java +++ b/ga-api/src/main/java/com/ga/api/service/settle/PaymentService.java @@ -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 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 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; + } } diff --git a/ga-api/src/main/java/com/ga/api/service/tax/TaxBreakdown.java b/ga-api/src/main/java/com/ga/api/service/tax/TaxBreakdown.java new file mode 100644 index 0000000..93d00ea --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/tax/TaxBreakdown.java @@ -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 +) {} diff --git a/ga-api/src/main/java/com/ga/api/service/tax/TaxCalculator.java b/ga-api/src/main/java/com/ga/api/service/tax/TaxCalculator.java new file mode 100644 index 0000000..a23ebc5 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/tax/TaxCalculator.java @@ -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")); + } +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/settle/PaymentMapper.java b/ga-core/src/main/java/com/ga/core/mapper/settle/PaymentMapper.java index 9406cd8..18e8f8e 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/settle/PaymentMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/settle/PaymentMapper.java @@ -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 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 list); List selectPendingByMonthAndBank(@Param("settleMonth") String settleMonth, @Param("bankCode") String bankCode); + List selectBySettleMonth(@Param("settleMonth") String settleMonth); }