동기화

This commit is contained in:
GA Pro
2026-05-15 00:25:18 +09:00
parent 88e14edffd
commit d5ed642c80
69 changed files with 2634 additions and 26 deletions
@@ -0,0 +1,36 @@
package com.ga.core.mapper.accounting;
import com.ga.core.vo.accounting.ContractAccountingEntryResp;
import com.ga.core.vo.accounting.ContractAccountingEntrySearchParam;
import com.ga.core.vo.accounting.ContractAccountingEntryVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ContractAccountingEntryMapper {
/** 목록 조회 — contractId / paymentId / entryDate 범위 / unpostedOnly 조건 */
List<ContractAccountingEntryResp> selectList(ContractAccountingEntrySearchParam param);
/** 단건 상세 */
ContractAccountingEntryResp selectDetailById(@Param("entryId") Long entryId);
/** 배치 step용: 정산월 기준 분개 대상 원장 조회 (recruit+maintain ledger UNION) */
List<ContractAccountingEntryVO> selectBySettleMonth(@Param("settleMonth") String settleMonth);
/**
* 분개 엔트리 일괄 INSERT.
* 회계 자동분개 배치 step에서 호출.
*/
int insertBatch(@Param("list") List<ContractAccountingEntryVO> list);
/**
* 미전기 엔트리 마감 처리 — journalNo 채번 후 UPDATE.
* @param ids 마감할 entry_id 목록
* @param journalNo 채번된 분개장 번호
*/
int closeJournal(@Param("ids") List<Long> ids,
@Param("journalNo") String journalNo);
}
@@ -0,0 +1,25 @@
package com.ga.core.mapper.income;
import com.ga.core.vo.income.AgentAnnualIncomeResp;
import com.ga.core.vo.income.AgentAnnualIncomeSearchParam;
import com.ga.core.vo.income.AgentAnnualIncomeVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AgentAnnualIncomeMapper {
/** 목록 조회 — settleYear / agentId / orgId 조건 */
List<AgentAnnualIncomeResp> selectList(AgentAnnualIncomeSearchParam param);
/** 단건 상세 (복합키: agentId + settleYear) */
AgentAnnualIncomeResp selectDetailById(@Param("agentId") Long agentId,
@Param("settleYear") int settleYear);
/**
* 연도 전체 UPSERT 집계 (전체 설계사).
*/
int upsertAggregate(@Param("year") int year);
}
@@ -0,0 +1,28 @@
package com.ga.core.mapper.tax;
import com.ga.core.vo.tax.WithholdingTaxReportResp;
import com.ga.core.vo.tax.WithholdingTaxReportSearchParam;
import com.ga.core.vo.tax.WithholdingTaxReportVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface WithholdingTaxReportMapper {
/** 목록 조회 — settleQuarter / agentId / orgId / fileStatus 조건 */
List<WithholdingTaxReportResp> selectList(WithholdingTaxReportSearchParam param);
/** 단건 상세 */
WithholdingTaxReportResp selectDetailById(@Param("reportId") Long reportId);
/**
* 분기 전체 UPSERT 집계.
* payment 테이블 기준 해당 분기 설계사별 SUM 후 INSERT … ON CONFLICT DO UPDATE.
*
* @param quarter 집계할 분기 (예: 2025-Q1)
* @return 처리된 행 수
*/
int upsertQuarterlyAggregate(@Param("quarter") String quarter);
}
@@ -0,0 +1,23 @@
package com.ga.core.vo.accounting;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 분개장 엑셀 다운로드 전용 VO
*/
@Data
public class ContractAccountingEntryExcelVO {
private String journalNo;
private LocalDate entryDate;
private String contractNo;
private String agentName;
private String debitAccount;
private String creditAccount;
private BigDecimal amount;
private String description;
private LocalDateTime createdAt;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.accounting;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 분개 엔트리 목록/상세 API 응답 — contract, agent 조인 포함
*/
@Data
public class ContractAccountingEntryResp {
private Long entryId;
private Long contractId;
private String contractNo;
private Long paymentId;
private Long agentId;
private String agentName;
private LocalDate entryDate;
private String debitAccount;
private String creditAccount;
private BigDecimal amount;
private String description;
/** NULL이면 미전기 상태 */
private String journalNo;
private LocalDateTime createdAt;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.accounting;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 분개 엔트리 수동 등록 요청 — 자동 분개 외 수기 입력용
*/
@Data
public class ContractAccountingEntrySaveReq {
@NotNull
private Long contractId;
private Long paymentId;
@NotNull
private LocalDate entryDate;
@NotBlank
private String debitAccount;
@NotBlank
private String creditAccount;
@NotNull
@DecimalMin("0.01")
private BigDecimal amount;
private String description;
}
@@ -0,0 +1,20 @@
package com.ga.core.vo.accounting;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
public class ContractAccountingEntrySearchParam extends SearchParam {
private Long contractId;
private Long paymentId;
private LocalDate entryDateFrom;
private LocalDate entryDateTo;
/** null이면 전체, ""이면 미전기(journalNo IS NULL) */
private String journalNo;
/** 미전기만 조회 플래그 */
private Boolean unpostedOnly;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.accounting;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* contract_accounting_entry — 계약 회계 분개 엔트리 (V64)
* INSERT/insertBatch/closeJournal 전용. 조인 필드 없음.
*/
@Data
public class ContractAccountingEntryVO {
private Long entryId;
private Long contractId;
private Long paymentId;
private LocalDate entryDate;
/** 차변 계정코드 (예: 8350 지급수수료) */
private String debitAccount;
/** 대변 계정코드 (예: 2530 미지급금) */
private String creditAccount;
private BigDecimal amount;
private String description;
/** 마감 시 채번. NULL=미전기 */
private String journalNo;
private LocalDateTime createdAt;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.income;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 연말 사업소득 집계 엑셀 다운로드 전용 VO
*/
@Data
public class AgentAnnualIncomeExcelVO {
private String agentName;
private String orgName;
private Integer settleYear;
private BigDecimal totalCommission;
private BigDecimal totalTaxWithheld;
private BigDecimal totalLocalTax;
private BigDecimal businessIncome;
private LocalDateTime generatedAt;
}
@@ -0,0 +1,25 @@
package com.ga.core.vo.income;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 연간소득 목록/상세 API 응답 — agent, organization 조인 포함
*/
@Data
public class AgentAnnualIncomeResp {
private Long agentId;
private String agentName;
private Long orgId;
private String orgName;
private Integer settleYear;
private BigDecimal totalCommission;
private BigDecimal totalTaxWithheld;
private BigDecimal totalLocalTax;
private BigDecimal businessIncome;
private LocalDateTime generatedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,17 @@
package com.ga.core.vo.income;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
/**
* 연간소득 재집계 트리거 요청 — 연도와 대상 설계사(선택)만 받음
*/
@Data
public class AgentAnnualIncomeSaveReq {
/** null이면 전체 설계사 집계 */
private Long agentId;
@NotNull
@Positive
private Integer settleYear;
}
@@ -0,0 +1,13 @@
package com.ga.core.vo.income;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class AgentAnnualIncomeSearchParam extends SearchParam {
private Long agentId;
private Integer settleYear;
private Long orgId;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.income;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* agent_annual_income — 설계사 연말 사업소득 집계 (V63)
* PK: (agent_id, settle_year) 복합키. INSERT/UPDATE 전용.
*/
@Data
public class AgentAnnualIncomeVO {
private Long agentId;
/** 정산 연도 (YYYY) */
private Integer settleYear;
/** 연간 총 수수료 합계 */
private BigDecimal totalCommission;
/** 원천징수 소득세 합계 */
private BigDecimal totalTaxWithheld;
/** 지방소득세 합계 */
private BigDecimal totalLocalTax;
/** 사업소득 = totalCommission - totalTaxWithheld - totalLocalTax */
private BigDecimal businessIncome;
/** 마지막 집계 실행 일시 */
private LocalDateTime generatedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -4,6 +4,7 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class ChargebackRuleVO {
@@ -18,4 +19,8 @@ public class ChargebackRuleVO {
private Integer maxInstallments;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;
}
@@ -19,4 +19,6 @@ public class CommissionRateVO {
private Integer version;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;
}
@@ -2,6 +2,8 @@ package com.ga.core.vo.rule;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ExceptionTypeCodeVO {
private String exceptionCode;
@@ -13,4 +15,8 @@ public class ExceptionTypeCodeVO {
private String taxIncluded;
private String description;
private String isActive;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;
}
@@ -4,6 +4,7 @@ import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class OverrideRuleVO {
@@ -16,4 +17,8 @@ public class OverrideRuleVO {
private BigDecimal capAmount;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;
}
@@ -20,4 +20,6 @@ public class PayoutRuleVO {
private Integer version;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.tax;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 원천세 분기신고 엑셀 다운로드 전용 VO
*/
@Data
public class WithholdingTaxReportExcelVO {
private String settleQuarter;
private String agentName;
private String orgName;
private BigDecimal totalIncome;
private BigDecimal totalWithheld;
private BigDecimal totalLocalTax;
private String fileStatus;
private LocalDateTime filedAt;
private LocalDateTime generatedAt;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.tax;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 원천세 분기신고 목록/상세 API 응답 — agent, organization 조인 포함
*/
@Data
public class WithholdingTaxReportResp {
private Long reportId;
private Long agentId;
private String agentName;
private Long orgId;
private String orgName;
private String settleQuarter;
private BigDecimal totalIncome;
private BigDecimal totalWithheld;
private BigDecimal totalLocalTax;
private LocalDateTime generatedAt;
private LocalDateTime filedAt;
private String fileStatus;
private String fileStatusName;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.tax;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 원천세 분기신고 재집계 트리거 요청
*/
@Data
public class WithholdingTaxReportSaveReq {
/** null이면 전체 설계사 집계 */
private Long agentId;
@NotBlank
private String settleQuarter;
}
@@ -0,0 +1,15 @@
package com.ga.core.vo.tax;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WithholdingTaxReportSearchParam extends SearchParam {
private String settleQuarter;
private Long agentId;
private Long orgId;
/** DRAFT / FILED / AMENDED */
private String fileStatus;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.tax;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* withholding_tax_report — 원천세 분기신고 (V67)
* INSERT/UPDATE 전용. 조인 필드 없음.
*/
@Data
public class WithholdingTaxReportVO {
private Long reportId;
private Long agentId;
/** 정산 분기 (예: 2025-Q1) */
private String settleQuarter;
private BigDecimal totalIncome;
private BigDecimal totalWithheld;
private BigDecimal totalLocalTax;
private LocalDateTime generatedAt;
private LocalDateTime filedAt;
/** DRAFT / FILED / AMENDED */
private String fileStatus;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.accounting.ContractAccountingEntryMapper">
<!-- contract, agent 조인 포함 응답 매핑 -->
<resultMap id="RespMap" type="com.ga.core.vo.accounting.ContractAccountingEntryResp"/>
<!-- 목록/상세 공통 SELECT 컬럼 -->
<sql id="joinCols">
cae.entry_id,
cae.contract_id,
ac.contract_no,
cae.payment_id,
a.agent_id,
a.agent_name,
cae.entry_date,
cae.debit_account,
cae.credit_account,
cae.amount,
cae.description,
cae.journal_no,
cae.created_at
</sql>
<!-- 공통 FROM + JOIN -->
<sql id="joinFrom">
FROM contract_accounting_entry cae
JOIN contract ac ON ac.contract_id = cae.contract_id
JOIN agent a ON a.agent_id = ac.agent_id
</sql>
<!-- ===== 목록 조회 ===== -->
<select id="selectList" resultMap="RespMap">
SELECT <include refid="joinCols"/>
<include refid="joinFrom"/>
<where>
<if test="contractId != null">AND cae.contract_id = #{contractId}</if>
<if test="paymentId != null">AND cae.payment_id = #{paymentId}</if>
<if test="entryDateFrom != null">AND cae.entry_date &gt;= #{entryDateFrom}</if>
<if test="entryDateTo != null">AND cae.entry_date &lt;= #{entryDateTo}</if>
<if test="journalNo != null and journalNo != ''">AND cae.journal_no = #{journalNo}</if>
<!-- unpostedOnly=true이면 미전기(journal_no IS NULL)만 조회 -->
<if test="unpostedOnly != null and unpostedOnly == true">
AND cae.journal_no IS NULL
</if>
<if test="searchKeyword != null and searchKeyword != ''">
AND (ac.contract_no LIKE CONCAT('%', #{searchKeyword}, '%')
OR a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%'))
</if>
</where>
ORDER BY cae.entry_date DESC, cae.entry_id DESC
</select>
<!-- ===== 단건 상세 ===== -->
<select id="selectDetailById" resultMap="RespMap">
SELECT <include refid="joinCols"/>
<include refid="joinFrom"/>
WHERE cae.entry_id = #{entryId}
</select>
<!-- ===== 일괄 INSERT =====
회계 자동분개 배치 step에서 한 번에 다수 엔트리 저장.
foreach로 멀티 VALUES 구문 생성.
-->
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="entryId">
INSERT INTO contract_accounting_entry
(contract_id, payment_id, entry_date,
debit_account, credit_account, amount, description)
VALUES
<foreach collection="list" item="e" separator=",">
(#{e.contractId}, #{e.paymentId}, #{e.entryDate},
#{e.debitAccount}, #{e.creditAccount}, #{e.amount}, #{e.description})
</foreach>
</insert>
<!-- ===== 마감 처리 (journalNo 채번) ===== -->
<update id="closeJournal">
UPDATE contract_accounting_entry
SET journal_no = #{journalNo}
WHERE entry_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND journal_no IS NULL
</update>
<!--
배치 step용: 정산월 기준 분개 대상 원장 조회.
recruit_ledger + maintain_ledger UNION → contract_id 포함.
AccountingEntryGenerateStep에서 호출.
-->
<select id="selectBySettleMonth" resultType="com.ga.core.vo.accounting.ContractAccountingEntryVO">
SELECT l.contract_id,
NULL::BIGINT AS payment_id,
CURRENT_DATE AS entry_date,
'8350' AS debit_account,
'2530' AS credit_account,
l.agent_amount AS amount,
CONCAT('모집수수료 ', l.settle_month) AS description,
NULL AS journal_no
FROM recruit_ledger l
WHERE l.settle_month = #{settleMonth}
AND l.agent_amount > 0
UNION ALL
SELECT l.contract_id,
NULL::BIGINT AS payment_id,
CURRENT_DATE AS entry_date,
'8350' AS debit_account,
'2530' AS credit_account,
l.agent_amount AS amount,
CONCAT('유지수수료 ', l.settle_month) AS description,
NULL AS journal_no
FROM maintain_ledger l
WHERE l.settle_month = #{settleMonth}
AND l.agent_amount > 0
</select>
</mapper>
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.income.AgentAnnualIncomeMapper">
<!-- agent 조인 포함 응답 매핑 -->
<resultMap id="RespMap" type="com.ga.core.vo.income.AgentAnnualIncomeResp"/>
<!-- 목록/상세에서 공통으로 사용하는 SELECT 컬럼 -->
<sql id="joinCols">
i.agent_id,
a.agent_name,
a.org_id,
o.org_name,
i.settle_year,
i.total_commission,
i.total_tax_withheld,
i.total_local_tax,
i.business_income,
i.generated_at,
i.created_at,
i.updated_at
</sql>
<!-- ===== 목록 조회 ===== -->
<select id="selectList" resultMap="RespMap">
SELECT <include refid="joinCols"/>
FROM agent_annual_income i
JOIN agent a ON a.agent_id = i.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
<where>
<if test="settleYear != null">AND i.settle_year = #{settleYear}</if>
<if test="agentId != null">AND i.agent_id = #{agentId}</if>
<if test="orgId != null">AND a.org_id = #{orgId}</if>
<if test="searchKeyword != null and searchKeyword != ''">
AND a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%')
</if>
</where>
ORDER BY i.settle_year DESC, i.agent_id ASC
</select>
<select id="selectDetailById" resultMap="RespMap">
SELECT <include refid="joinCols"/>
FROM agent_annual_income i
JOIN agent a ON a.agent_id = i.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
WHERE i.agent_id = #{agentId}
AND i.settle_year = #{settleYear}
</select>
<!-- 연도 전체 UPSERT 집계: payment 테이블 기준 설계사별 SUM 후 UPSERT -->
<insert id="upsertAggregate">
INSERT INTO agent_annual_income
(agent_id, settle_year,
total_commission, total_tax_withheld, total_local_tax, business_income,
generated_at)
SELECT
p.agent_id,
#{year} AS settle_year,
COALESCE(SUM(p.pay_amount), 0) AS total_commission,
COALESCE(SUM(p.income_tax_amount), 0) AS total_tax_withheld,
COALESCE(SUM(p.local_tax_amount), 0) AS total_local_tax,
COALESCE(SUM(p.pay_amount)
- SUM(p.income_tax_amount)
- SUM(p.local_tax_amount), 0) AS business_income,
NOW() AS generated_at
FROM payment p
WHERE EXTRACT(YEAR FROM p.pay_date) = #{year}
GROUP BY p.agent_id
ON CONFLICT (agent_id, settle_year) DO UPDATE SET
total_commission = EXCLUDED.total_commission,
total_tax_withheld = EXCLUDED.total_tax_withheld,
total_local_tax = EXCLUDED.total_local_tax,
business_income = EXCLUDED.business_income,
generated_at = NOW(),
updated_at = NOW()
</insert>
</mapper>
@@ -92,4 +92,51 @@
<foreach collection="paymentIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</update>
<select id="selectBySettleMonth" resultMap="VOMap">
SELECT p.*
FROM payment p
JOIN settle_master s ON s.settle_id = p.settle_id
WHERE s.settle_month = #{settleMonth}
ORDER BY p.payment_id
</select>
<select id="selectByAgentAndMonth" resultMap="VOMap">
SELECT p.*
FROM payment p
JOIN settle_master s ON s.settle_id = p.settle_id
WHERE p.agent_id = #{agentId}
AND s.settle_month = #{settleMonth}
ORDER BY p.payment_id
</select>
<update id="updateTaxes">
UPDATE payment
SET income_tax_amount = #{incomeTaxAmount},
local_tax_amount = #{localTaxAmount},
vat_amount = #{vatAmount},
net_amount = #{netAmount},
updated_at = NOW()
WHERE payment_id = #{paymentId}
</update>
<update id="updateTaxesBatch">
<foreach collection="list" item="p" separator=";">
UPDATE payment
SET income_tax_amount = #{p.incomeTaxAmount},
local_tax_amount = #{p.localTaxAmount},
vat_amount = #{p.vatAmount},
net_amount = #{p.netAmount},
updated_at = NOW()
WHERE payment_id = #{p.paymentId}
</foreach>
</update>
<update id="updateDeduction">
UPDATE payment
SET deduction_amount = #{deductionAmount},
net_amount = #{netAmount},
updated_at = NOW()
WHERE payment_id = #{paymentId}
</update>
</mapper>