feat: P5-W3 본론 완료 — 회계결산/이상치예측/연말명세서 (V74~V78)
P5-W2 원천세·부가세 분기신고 미커밋분 + P5-W3 메뉴/권한 보강(V74) + P5-W3 본론(V75~V78)을 일괄 정리. - V75 accounting_close / account_balance_snapshot (회계 결산) - V76 retention_anomaly_alert / forecast_kpi_monthly (이상치·예측 KPI) - V77 year_end_statement (연말 지급명세서) - V78 P5-W3 본론 메뉴 4 + 권한 + 공통코드 7그룹 + 테스트데이터 - api: AccountingClose/YearEndStatement/ForecastKpi/RetentionAnomaly Service+Controller - batch: BatchConfig Step 11/12 (원천세·부가세 분기) 연결 - frontend: 4화면(AccountingClose/YearEndStatement/RetentionAnomaly/ForecastKpi) + API 모듈 4 + 라우트 - frontend: usePermission/PermissionButton EXECUTE 권한 누락 보강 - fix: YearEndStatementMapper.xml 미존재 컬럼 a.agent_code 참조 제거 통합검증: Flyway V74~V78 운영DB 적용(v78), 4모듈 compileJava + 프론트 build PASS, 스모크 GET 8/8 + 쓰기 4/4 PASS, verify_v74.py 20/20 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package com.ga.core.mapper.accounting;
|
||||
|
||||
import com.ga.core.vo.accounting.AccountBalanceSnapshotVO;
|
||||
import com.ga.core.vo.accounting.AccountingCloseResp;
|
||||
import com.ga.core.vo.accounting.AccountingCloseSearchParam;
|
||||
import com.ga.core.vo.accounting.AccountingCloseVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AccountingCloseMapper {
|
||||
|
||||
/** 결산 목록 */
|
||||
List<AccountingCloseResp> selectList(AccountingCloseSearchParam param);
|
||||
|
||||
/** 결산 단건 상세 */
|
||||
AccountingCloseResp selectDetailById(@Param("closeId") Long closeId);
|
||||
|
||||
/** 결산 헤더 INSERT (DRAFT 상태) */
|
||||
int insert(AccountingCloseVO vo);
|
||||
|
||||
/**
|
||||
* 마감 처리.
|
||||
* - close_status = CLOSED, closed_at = now(), closed_by, journal_count/total_debit/total_credit UPDATE
|
||||
*/
|
||||
int updateClose(@Param("closeId") Long closeId,
|
||||
@Param("journalCount") int journalCount,
|
||||
@Param("totalDebit") BigDecimal totalDebit,
|
||||
@Param("totalCredit") BigDecimal totalCredit,
|
||||
@Param("closedBy") Long closedBy);
|
||||
|
||||
/**
|
||||
* 재오픈 처리.
|
||||
* - close_status = REOPENED, reopened_at = now(), reopened_by, reopen_reason UPDATE
|
||||
*/
|
||||
int updateReopen(@Param("closeId") Long closeId,
|
||||
@Param("reopenedBy") Long reopenedBy,
|
||||
@Param("reopenReason") String reopenReason);
|
||||
|
||||
/**
|
||||
* 마감 대상 미전기 분개 엔트리 ID 목록 조회.
|
||||
* - 월결산: entry_date가 YYYY-MM 범위 & journal_no IS NULL
|
||||
* - 분기/연결산: 해당 분기/연도 범위
|
||||
*/
|
||||
List<Long> selectUnpostedEntryIds(@Param("closeType") String closeType,
|
||||
@Param("closePeriod") String closePeriod);
|
||||
|
||||
/** 결산 시점 잔액 스냅샷 일괄 INSERT (debit/credit 합계 + balance) */
|
||||
int insertBalanceSnapshot(@Param("list") List<AccountBalanceSnapshotVO> list);
|
||||
|
||||
/**
|
||||
* 결산 시점 계정과목별 차변/대변 합계 집계.
|
||||
* contract_accounting_entry 에서 journal_no가 채번된 분개를 account_code 별로 SUM.
|
||||
*/
|
||||
List<AccountBalanceSnapshotVO> selectBalanceAggregate(@Param("journalPrefix") String journalPrefix);
|
||||
|
||||
/** 단순 잔액 스냅샷 목록 (closeId 기준) */
|
||||
List<AccountBalanceSnapshotVO> selectSnapshotByCloseId(@Param("closeId") Long closeId);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ga.core.mapper.income;
|
||||
|
||||
import com.ga.core.vo.income.YearEndStatementExcelVO;
|
||||
import com.ga.core.vo.income.YearEndStatementResp;
|
||||
import com.ga.core.vo.income.YearEndStatementSearchParam;
|
||||
import com.ga.core.vo.income.YearEndStatementVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.cursor.Cursor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface YearEndStatementMapper {
|
||||
|
||||
List<YearEndStatementResp> selectList(YearEndStatementSearchParam param);
|
||||
|
||||
YearEndStatementResp selectDetailById(@Param("statementId") Long statementId);
|
||||
|
||||
/** 엑셀 다운로드 전용 (cursor 스트리밍) */
|
||||
Cursor<YearEndStatementExcelVO> selectExcelCursor(YearEndStatementSearchParam param);
|
||||
|
||||
/**
|
||||
* 연도 전체 UPSERT 생성.
|
||||
* agent_annual_income 데이터를 가져와 statement 행 생성/갱신.
|
||||
* agentIds 가 null 이면 전체 설계사 대상.
|
||||
*/
|
||||
int upsertByYear(@Param("year") int year,
|
||||
@Param("agentIds") List<Long> agentIds);
|
||||
|
||||
/** 발급 처리 — file_path/file_format/file_status='GENERATED'/generated_at UPDATE */
|
||||
int updateGenerated(@Param("statementId") Long statementId,
|
||||
@Param("filePath") String filePath,
|
||||
@Param("fileFormat") String fileFormat);
|
||||
|
||||
/** 발송 처리 — ids 일괄 UPDATE: status=SENT, sent_at=now() */
|
||||
int updateSent(@Param("ids") List<Long> ids,
|
||||
@Param("sentTo") String sentTo);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ga.core.mapper.kpi;
|
||||
|
||||
import com.ga.core.vo.kpi.ForecastKpiMonthlyResp;
|
||||
import com.ga.core.vo.kpi.ForecastKpiMonthlySearchParam;
|
||||
import com.ga.core.vo.kpi.ForecastKpiMonthlyVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ForecastKpiMonthlyMapper {
|
||||
|
||||
/** 예측 목록 */
|
||||
List<ForecastKpiMonthlyResp> selectList(ForecastKpiMonthlySearchParam param);
|
||||
|
||||
/**
|
||||
* UPSERT — 동일 (forecast_month, target_org_id, metric_code) 조합은 갱신.
|
||||
* target_org_id NULL 도 부분 UNIQUE 인덱스로 처리.
|
||||
*/
|
||||
int upsertBatch(@Param("list") List<ForecastKpiMonthlyVO> list);
|
||||
|
||||
/**
|
||||
* 차월 예측 산출 — mv_monthly_summary / mv_retention_rate 기반.
|
||||
* 입력 forecastMonth(YYYY-MM) 직전 sampleSize 개월 평균/표준편차.
|
||||
*/
|
||||
List<ForecastKpiMonthlyVO> computeForecast(@Param("forecastMonth") String forecastMonth,
|
||||
@Param("method") String method,
|
||||
@Param("sampleSize") int sampleSize);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ga.core.mapper.kpi;
|
||||
|
||||
import com.ga.core.vo.kpi.RetentionAnomalyAlertResp;
|
||||
import com.ga.core.vo.kpi.RetentionAnomalyAlertSearchParam;
|
||||
import com.ga.core.vo.kpi.RetentionAnomalyAlertVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface RetentionAnomalyAlertMapper {
|
||||
|
||||
/** 이상치 목록 */
|
||||
List<RetentionAnomalyAlertResp> selectList(RetentionAnomalyAlertSearchParam param);
|
||||
|
||||
/** 단건 상세 */
|
||||
RetentionAnomalyAlertResp selectDetailById(@Param("alertId") Long alertId);
|
||||
|
||||
/** 일괄 INSERT — 탐지 배치 결과 저장 */
|
||||
int insertBatch(@Param("list") List<RetentionAnomalyAlertVO> list);
|
||||
|
||||
/**
|
||||
* 검토/완료 처리.
|
||||
* status: REVIEWED 또는 RESOLVED.
|
||||
*/
|
||||
int updateStatus(@Param("ids") List<Long> ids,
|
||||
@Param("status") String status,
|
||||
@Param("note") String note,
|
||||
@Param("reviewedBy") Long reviewedBy);
|
||||
|
||||
/**
|
||||
* 이상치 탐지 — 입력 월 기준 mv_retention_rate 직전 3개월 평균 대비 1.5%P 이상 편차.
|
||||
* 결과는 호출측에서 insertBatch로 저장 (INSERT 직접 안 함, UNIQUE 충돌 방지).
|
||||
*/
|
||||
List<RetentionAnomalyAlertVO> detectAnomalies(@Param("alertMonth") String alertMonth);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ga.core.mapper.tax;
|
||||
|
||||
import com.ga.core.vo.tax.VatReportResp;
|
||||
import com.ga.core.vo.tax.VatReportSearchParam;
|
||||
import com.ga.core.vo.tax.VatReportVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface VatReportMapper {
|
||||
|
||||
/** 목록 조회 — settleQuarter / fileStatus 조건 */
|
||||
List<VatReportResp> selectList(VatReportSearchParam param);
|
||||
|
||||
/** 단건 상세 */
|
||||
VatReportResp selectDetailById(@Param("vatId") Long vatId);
|
||||
|
||||
/**
|
||||
* 분기 전체 UPSERT 집계.
|
||||
* tax_invoice 테이블 기준 해당 분기 SUPPLY/PURCHASE SUM 후 INSERT … ON CONFLICT DO UPDATE.
|
||||
*
|
||||
* @param quarter 집계할 분기 (예: 2025-Q1)
|
||||
* @return 처리된 행 수
|
||||
*/
|
||||
int upsertAggregate(@Param("quarter") String quarter);
|
||||
|
||||
/** 신고 완료 처리 — file_status=FILED, filed_at=now(). 호출측에서 빈 리스트 방어. */
|
||||
int updateFileStatusToFiled(@Param("ids") List<Long> ids);
|
||||
}
|
||||
@@ -25,4 +25,7 @@ public interface WithholdingTaxReportMapper {
|
||||
* @return 처리된 행 수
|
||||
*/
|
||||
int upsertQuarterlyAggregate(@Param("quarter") String quarter);
|
||||
|
||||
/** 신고 완료 처리 — file_status=FILED, filed_at=now(). 호출측에서 빈 리스트 방어. */
|
||||
int updateFileStatusToFiled(@Param("ids") List<Long> ids);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* account_balance_snapshot — 결산 시점 계정과목별 잔액 스냅샷 (V75)
|
||||
*/
|
||||
@Data
|
||||
public class AccountBalanceSnapshotVO {
|
||||
private Long snapshotId;
|
||||
private Long closeId;
|
||||
private String accountCode;
|
||||
private String accountName;
|
||||
/** DEBIT / CREDIT / BOTH */
|
||||
private String accountType;
|
||||
private BigDecimal debitTotal;
|
||||
private BigDecimal creditTotal;
|
||||
/** debit_total - credit_total */
|
||||
private BigDecimal balance;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import com.ga.common.annotation.ExcelColumn;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class AccountingCloseExcelVO {
|
||||
@ExcelColumn(header = "결산기간", order = 1)
|
||||
private String closePeriod;
|
||||
@ExcelColumn(header = "구분", order = 2)
|
||||
private String closeTypeName;
|
||||
@ExcelColumn(header = "상태", order = 3)
|
||||
private String closeStatusName;
|
||||
@ExcelColumn(header = "분개수", order = 4)
|
||||
private Integer journalCount;
|
||||
@ExcelColumn(header = "차변합", order = 5)
|
||||
private BigDecimal totalDebit;
|
||||
@ExcelColumn(header = "대변합", order = 6)
|
||||
private BigDecimal totalCredit;
|
||||
@ExcelColumn(header = "마감자", order = 7)
|
||||
private String closedByName;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 회계 결산 목록/상세 API 응답. 공통코드명, 처리자명 조인 포함.
|
||||
*/
|
||||
@Data
|
||||
public class AccountingCloseResp {
|
||||
private Long closeId;
|
||||
private String closePeriod;
|
||||
private String closeType;
|
||||
private String closeTypeName;
|
||||
private String closeStatus;
|
||||
private String closeStatusName;
|
||||
private String journalPrefix;
|
||||
private Integer journalCount;
|
||||
private BigDecimal totalDebit;
|
||||
private BigDecimal totalCredit;
|
||||
private BigDecimal balanceDiff;
|
||||
private LocalDateTime closedAt;
|
||||
private Long closedBy;
|
||||
private String closedByName;
|
||||
private LocalDateTime reopenedAt;
|
||||
private Long reopenedBy;
|
||||
private String reopenedByName;
|
||||
private String reopenReason;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 회계 결산 생성/실행 요청.
|
||||
* - createDraft: closePeriod + closeType 지정해 DRAFT 행 생성
|
||||
* - close : closeId 지정해 마감 실행 (분개 채번 + 잔액 스냅샷)
|
||||
* - reopen : closeId + reopenReason
|
||||
*/
|
||||
@Data
|
||||
public class AccountingCloseSaveReq {
|
||||
private Long closeId;
|
||||
/** YYYY-MM / YYYY-Q1 / YYYY */
|
||||
@NotBlank
|
||||
private String closePeriod;
|
||||
/** MONTHLY / QUARTERLY / YEARLY */
|
||||
@NotBlank
|
||||
private String closeType;
|
||||
/** reopen 사유 (REOPENED 처리 시 필수) */
|
||||
private String reopenReason;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AccountingCloseSearchParam extends SearchParam {
|
||||
private String closePeriod;
|
||||
/** MONTHLY / QUARTERLY / YEARLY */
|
||||
private String closeType;
|
||||
/** DRAFT / CLOSED / REOPENED */
|
||||
private String closeStatus;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ga.core.vo.accounting;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* accounting_close — 회계 결산 헤더 (V75)
|
||||
* INSERT / UPDATE 전용. 조인 필드 없음.
|
||||
*/
|
||||
@Data
|
||||
public class AccountingCloseVO {
|
||||
private Long closeId;
|
||||
/** 결산 기간 키 (월: YYYY-MM, 분기: YYYY-Q1, 연: YYYY) */
|
||||
private String closePeriod;
|
||||
/** MONTHLY / QUARTERLY / YEARLY */
|
||||
private String closeType;
|
||||
/** DRAFT / CLOSED / REOPENED */
|
||||
private String closeStatus;
|
||||
/** 분개 채번 prefix */
|
||||
private String journalPrefix;
|
||||
private Integer journalCount;
|
||||
private BigDecimal totalDebit;
|
||||
private BigDecimal totalCredit;
|
||||
private LocalDateTime closedAt;
|
||||
private Long closedBy;
|
||||
private LocalDateTime reopenedAt;
|
||||
private Long reopenedBy;
|
||||
private String reopenReason;
|
||||
private LocalDateTime createdAt;
|
||||
private Long createdBy;
|
||||
private LocalDateTime updatedAt;
|
||||
private Long updatedBy;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ga.core.vo.income;
|
||||
|
||||
import com.ga.common.annotation.ExcelColumn;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class YearEndStatementExcelVO {
|
||||
@ExcelColumn(header = "설계사코드", order = 1)
|
||||
private String agentCode;
|
||||
@ExcelColumn(header = "설계사명", order = 2)
|
||||
private String agentName;
|
||||
@ExcelColumn(header = "소속조직", order = 3)
|
||||
private String orgName;
|
||||
@ExcelColumn(header = "사업자번호", order = 4)
|
||||
private String businessNo;
|
||||
@ExcelColumn(header = "신고연도", order = 5)
|
||||
private Integer statementYear;
|
||||
@ExcelColumn(header = "총지급액", order = 6)
|
||||
private BigDecimal totalIncome;
|
||||
@ExcelColumn(header = "원천세", order = 7)
|
||||
private BigDecimal totalWithheld;
|
||||
@ExcelColumn(header = "지방세", order = 8)
|
||||
private BigDecimal totalLocalTax;
|
||||
@ExcelColumn(header = "상태", order = 9)
|
||||
private String fileStatusName;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ga.core.vo.income;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class YearEndStatementResp {
|
||||
private Long statementId;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private String agentCode;
|
||||
private String businessNo;
|
||||
private Long orgId;
|
||||
private String orgName;
|
||||
private Integer statementYear;
|
||||
private String statementType;
|
||||
private String statementTypeName;
|
||||
private BigDecimal totalIncome;
|
||||
private BigDecimal totalWithheld;
|
||||
private BigDecimal totalLocalTax;
|
||||
private String filePath;
|
||||
private String fileFormat;
|
||||
private String fileStatus;
|
||||
private String fileStatusName;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime sentAt;
|
||||
private String sentTo;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ga.core.vo.income;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 연말 지급명세서 생성/발급/발송 요청.
|
||||
* - generate: statementYear, agentIds(빈 리스트면 전체)
|
||||
* - send : ids + sentTo
|
||||
*/
|
||||
@Data
|
||||
public class YearEndStatementSaveReq {
|
||||
private Integer statementYear;
|
||||
private List<Long> agentIds;
|
||||
private String statementType;
|
||||
private String fileFormat;
|
||||
private List<Long> ids;
|
||||
private String sentTo;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ga.core.vo.income;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class YearEndStatementSearchParam extends SearchParam {
|
||||
private Integer statementYear;
|
||||
private Long agentId;
|
||||
private Long orgId;
|
||||
private String fileStatus;
|
||||
private String fileFormat;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ga.core.vo.income;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* year_end_statement — 설계사 연말 지급명세서 (V77)
|
||||
*/
|
||||
@Data
|
||||
public class YearEndStatementVO {
|
||||
private Long statementId;
|
||||
private Long agentId;
|
||||
private Integer statementYear;
|
||||
/** INCOME_DEDUCTION */
|
||||
private String statementType;
|
||||
private BigDecimal totalIncome;
|
||||
private BigDecimal totalWithheld;
|
||||
private BigDecimal totalLocalTax;
|
||||
private String filePath;
|
||||
/** XLSX / PDF */
|
||||
private String fileFormat;
|
||||
/** DRAFT / GENERATED / SENT */
|
||||
private String fileStatus;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime sentAt;
|
||||
private String sentTo;
|
||||
private LocalDateTime createdAt;
|
||||
private Long createdBy;
|
||||
private LocalDateTime updatedAt;
|
||||
private Long updatedBy;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class ForecastKpiMonthlyResp {
|
||||
private Long forecastId;
|
||||
private String forecastMonth;
|
||||
private Long targetOrgId;
|
||||
private String targetOrgName;
|
||||
private String metricCode;
|
||||
private String metricName;
|
||||
private BigDecimal forecastValue;
|
||||
private BigDecimal lowerBound;
|
||||
private BigDecimal upperBound;
|
||||
private String method;
|
||||
private String methodName;
|
||||
private String basePeriod;
|
||||
private Integer sampleCount;
|
||||
private LocalDateTime generatedAt;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 차월 KPI 예측 재산출 요청.
|
||||
* - forecastMonth: 산출 대상 월
|
||||
* - method: 없으면 SMA3 기본
|
||||
*/
|
||||
@Data
|
||||
public class ForecastKpiMonthlySaveReq {
|
||||
@NotBlank
|
||||
private String forecastMonth;
|
||||
/** SMA3 / SMA6 / NAIVE — null 이면 SMA3 */
|
||||
private String method;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ForecastKpiMonthlySearchParam extends SearchParam {
|
||||
private String forecastMonth;
|
||||
private Long targetOrgId;
|
||||
private String metricCode;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* forecast_kpi_monthly — 차월 KPI 예측 (V76)
|
||||
*/
|
||||
@Data
|
||||
public class ForecastKpiMonthlyVO {
|
||||
private Long forecastId;
|
||||
/** 예측 대상 월 YYYY-MM */
|
||||
private String forecastMonth;
|
||||
/** NULL = 전사 */
|
||||
private Long targetOrgId;
|
||||
/** MONTHLY_PREMIUM/MONTHLY_PAYOUT/RETENTION_13M/RETENTION_25M */
|
||||
private String metricCode;
|
||||
private BigDecimal forecastValue;
|
||||
private BigDecimal lowerBound;
|
||||
private BigDecimal upperBound;
|
||||
/** SMA3 / SMA6 / NAIVE */
|
||||
private String method;
|
||||
private String basePeriod;
|
||||
private Integer sampleCount;
|
||||
private LocalDateTime generatedAt;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class RetentionAnomalyAlertResp {
|
||||
private Long alertId;
|
||||
private String targetType;
|
||||
private String targetTypeName;
|
||||
private Long targetId;
|
||||
/** AGENT면 agent_name, ORG면 org_name, ALL이면 "전사" */
|
||||
private String targetName;
|
||||
private String alertMonth;
|
||||
private String retentionBand;
|
||||
private BigDecimal expectedRate;
|
||||
private BigDecimal actualRate;
|
||||
private BigDecimal deviation;
|
||||
private String severity;
|
||||
private String severityName;
|
||||
private String status;
|
||||
private String statusName;
|
||||
private String note;
|
||||
private LocalDateTime detectedAt;
|
||||
private LocalDateTime reviewedAt;
|
||||
private Long reviewedBy;
|
||||
private String reviewedByName;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 이상치 검토/완료 처리 요청.
|
||||
* - review : 다건 ID + status (REVIEWED/RESOLVED) + note
|
||||
* - detect : alertMonth 단일 — 재탐지 트리거
|
||||
*/
|
||||
@Data
|
||||
public class RetentionAnomalyAlertSaveReq {
|
||||
/** detect 트리거에 사용 */
|
||||
private String alertMonth;
|
||||
/** review 처리에 사용 */
|
||||
private List<Long> ids;
|
||||
/** NEW / REVIEWED / RESOLVED */
|
||||
private String status;
|
||||
private String note;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class RetentionAnomalyAlertSearchParam extends SearchParam {
|
||||
private String alertMonth;
|
||||
private String targetType;
|
||||
private String retentionBand;
|
||||
private String severity;
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ga.core.vo.kpi;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* retention_anomaly_alert — 13M/25M 유지율 이상치 (V76)
|
||||
*/
|
||||
@Data
|
||||
public class RetentionAnomalyAlertVO {
|
||||
private Long alertId;
|
||||
/** AGENT / ORG / ALL */
|
||||
private String targetType;
|
||||
/** ALL 일 때 null */
|
||||
private Long targetId;
|
||||
/** YYYY-MM */
|
||||
private String alertMonth;
|
||||
/** 13M / 25M */
|
||||
private String retentionBand;
|
||||
private BigDecimal expectedRate;
|
||||
private BigDecimal actualRate;
|
||||
private BigDecimal deviation;
|
||||
/** LOW / MED / HIGH */
|
||||
private String severity;
|
||||
/** NEW / REVIEWED / RESOLVED */
|
||||
private String status;
|
||||
private String note;
|
||||
private LocalDateTime detectedAt;
|
||||
private LocalDateTime reviewedAt;
|
||||
private Long reviewedBy;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ga.core.vo.tax;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 부가세 분기신고 엑셀 다운로드 전용 VO
|
||||
*/
|
||||
@Data
|
||||
public class VatReportExcelVO {
|
||||
private String settleQuarter;
|
||||
private BigDecimal totalSales;
|
||||
private BigDecimal totalPurchase;
|
||||
private BigDecimal totalVatPayable;
|
||||
private Integer taxInvoiceCount;
|
||||
private String fileStatus;
|
||||
private LocalDateTime filedAt;
|
||||
private LocalDateTime generatedAt;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ga.core.vo.tax;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 부가세 분기신고 목록/상세 API 응답
|
||||
*/
|
||||
@Data
|
||||
public class VatReportResp {
|
||||
private Long vatId;
|
||||
private String settleQuarter;
|
||||
private BigDecimal totalSales;
|
||||
private BigDecimal totalPurchase;
|
||||
private BigDecimal totalVatPayable;
|
||||
private Integer taxInvoiceCount;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime filedAt;
|
||||
private String fileStatus;
|
||||
private String fileStatusName;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ga.core.vo.tax;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 부가세 분기신고 재집계 트리거 요청
|
||||
*/
|
||||
@Data
|
||||
public class VatReportSaveReq {
|
||||
@NotBlank
|
||||
private String settleQuarter;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ga.core.vo.tax;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VatReportSearchParam extends SearchParam {
|
||||
private String settleQuarter;
|
||||
/** 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;
|
||||
|
||||
/**
|
||||
* vat_report — 부가세 분기신고 (V68)
|
||||
* INSERT/UPDATE 전용. 조인 필드 없음.
|
||||
*/
|
||||
@Data
|
||||
public class VatReportVO {
|
||||
private Long vatId;
|
||||
/** 정산 분기 (예: 2025-Q1) */
|
||||
private String settleQuarter;
|
||||
private BigDecimal totalSales;
|
||||
private BigDecimal totalPurchase;
|
||||
private BigDecimal totalVatPayable;
|
||||
private Integer taxInvoiceCount;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime filedAt;
|
||||
/** DRAFT / FILED / AMENDED */
|
||||
private String fileStatus;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?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.AccountingCloseMapper">
|
||||
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.accounting.AccountingCloseResp"/>
|
||||
<resultMap id="SnapMap" type="com.ga.core.vo.accounting.AccountBalanceSnapshotVO"/>
|
||||
|
||||
<sql id="joinCols">
|
||||
ac.close_id,
|
||||
ac.close_period,
|
||||
ac.close_type,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'ACC_CLOSE_TYPE' AND cc.code = ac.close_type) AS close_type_name,
|
||||
ac.close_status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'ACC_CLOSE_STATUS' AND cc.code = ac.close_status) AS close_status_name,
|
||||
ac.journal_prefix,
|
||||
ac.journal_count,
|
||||
ac.total_debit,
|
||||
ac.total_credit,
|
||||
(ac.total_debit - ac.total_credit) AS balance_diff,
|
||||
ac.closed_at,
|
||||
ac.closed_by,
|
||||
uc.user_name AS closed_by_name,
|
||||
ac.reopened_at,
|
||||
ac.reopened_by,
|
||||
ur.user_name AS reopened_by_name,
|
||||
ac.reopen_reason,
|
||||
ac.created_at,
|
||||
ac.updated_at
|
||||
</sql>
|
||||
|
||||
<sql id="joinFrom">
|
||||
FROM accounting_close ac
|
||||
LEFT JOIN users uc ON uc.user_id = ac.closed_by
|
||||
LEFT JOIN users ur ON ur.user_id = ac.reopened_by
|
||||
</sql>
|
||||
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
<include refid="joinFrom"/>
|
||||
<where>
|
||||
<if test="closePeriod != null and closePeriod != ''">AND ac.close_period = #{closePeriod}</if>
|
||||
<if test="closeType != null and closeType != ''">AND ac.close_type = #{closeType}</if>
|
||||
<if test="closeStatus != null and closeStatus != ''">AND ac.close_status = #{closeStatus}</if>
|
||||
</where>
|
||||
ORDER BY ac.close_period DESC, ac.close_type
|
||||
</select>
|
||||
|
||||
<select id="selectDetailById" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
<include refid="joinFrom"/>
|
||||
WHERE ac.close_id = #{closeId}
|
||||
</select>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="closeId">
|
||||
INSERT INTO accounting_close
|
||||
(close_period, close_type, close_status, journal_prefix,
|
||||
journal_count, total_debit, total_credit,
|
||||
created_at, created_by, updated_at)
|
||||
VALUES
|
||||
(#{closePeriod}, #{closeType}, COALESCE(#{closeStatus}, 'DRAFT'), #{journalPrefix},
|
||||
0, 0, 0,
|
||||
now(), #{createdBy}, now())
|
||||
</insert>
|
||||
|
||||
<update id="updateClose">
|
||||
UPDATE accounting_close
|
||||
SET close_status = 'CLOSED',
|
||||
closed_at = now(),
|
||||
closed_by = #{closedBy},
|
||||
journal_count = #{journalCount},
|
||||
total_debit = #{totalDebit},
|
||||
total_credit = #{totalCredit},
|
||||
updated_at = now(),
|
||||
updated_by = #{closedBy}
|
||||
WHERE close_id = #{closeId}
|
||||
AND close_status IN ('DRAFT','REOPENED')
|
||||
</update>
|
||||
|
||||
<update id="updateReopen">
|
||||
UPDATE accounting_close
|
||||
SET close_status = 'REOPENED',
|
||||
reopened_at = now(),
|
||||
reopened_by = #{reopenedBy},
|
||||
reopen_reason = #{reopenReason},
|
||||
updated_at = now(),
|
||||
updated_by = #{reopenedBy}
|
||||
WHERE close_id = #{closeId}
|
||||
AND close_status = 'CLOSED'
|
||||
</update>
|
||||
|
||||
<!--
|
||||
마감 대상 미전기 분개 ID 목록.
|
||||
- MONTHLY: YYYY-MM → entry_date를 to_char(YYYY-MM)으로 비교
|
||||
- QUARTERLY: YYYY-Qn → EXTRACT(QUARTER) 비교
|
||||
- YEARLY: YYYY → EXTRACT(YEAR) 비교
|
||||
-->
|
||||
<select id="selectUnpostedEntryIds" resultType="java.lang.Long">
|
||||
SELECT cae.entry_id
|
||||
FROM contract_accounting_entry cae
|
||||
WHERE cae.journal_no IS NULL
|
||||
<choose>
|
||||
<when test="closeType == 'MONTHLY'">
|
||||
AND to_char(cae.entry_date, 'YYYY-MM') = #{closePeriod}
|
||||
</when>
|
||||
<when test="closeType == 'QUARTERLY'">
|
||||
AND EXTRACT(YEAR FROM cae.entry_date) = CAST(SPLIT_PART(#{closePeriod}, '-Q', 1) AS INT)
|
||||
AND EXTRACT(QUARTER FROM cae.entry_date) = CAST(SPLIT_PART(#{closePeriod}, '-Q', 2) AS INT)
|
||||
</when>
|
||||
<when test="closeType == 'YEARLY'">
|
||||
AND EXTRACT(YEAR FROM cae.entry_date) = CAST(#{closePeriod} AS INT)
|
||||
</when>
|
||||
</choose>
|
||||
ORDER BY cae.entry_date, cae.entry_id
|
||||
</select>
|
||||
|
||||
<!--
|
||||
결산 시점 계정과목별 차변/대변 합계 집계.
|
||||
journal_prefix로 시작하는 journal_no 분개를 account_code 별로 SUM.
|
||||
debit_account / credit_account를 별도 SELECT로 합산 후 FULL OUTER JOIN.
|
||||
-->
|
||||
<select id="selectBalanceAggregate" resultMap="SnapMap">
|
||||
WITH d AS (
|
||||
SELECT cae.debit_account AS account_code, SUM(cae.amount) AS debit_total
|
||||
FROM contract_accounting_entry cae
|
||||
WHERE cae.journal_no LIKE CONCAT(#{journalPrefix}, '-%')
|
||||
GROUP BY cae.debit_account
|
||||
), c AS (
|
||||
SELECT cae.credit_account AS account_code, SUM(cae.amount) AS credit_total
|
||||
FROM contract_accounting_entry cae
|
||||
WHERE cae.journal_no LIKE CONCAT(#{journalPrefix}, '-%')
|
||||
GROUP BY cae.credit_account
|
||||
)
|
||||
SELECT COALESCE(d.account_code, c.account_code) AS account_code,
|
||||
ac.account_name,
|
||||
ac.account_type,
|
||||
COALESCE(d.debit_total, 0) AS debit_total,
|
||||
COALESCE(c.credit_total, 0) AS credit_total,
|
||||
COALESCE(d.debit_total, 0) - COALESCE(c.credit_total, 0) AS balance
|
||||
FROM d
|
||||
FULL OUTER JOIN c ON c.account_code = d.account_code
|
||||
LEFT JOIN account_code ac ON ac.account_code = COALESCE(d.account_code, c.account_code)
|
||||
ORDER BY 1
|
||||
</select>
|
||||
|
||||
<insert id="insertBalanceSnapshot">
|
||||
INSERT INTO account_balance_snapshot
|
||||
(close_id, account_code, account_name, account_type,
|
||||
debit_total, credit_total, balance)
|
||||
VALUES
|
||||
<foreach collection="list" item="s" separator=",">
|
||||
(#{s.closeId}, #{s.accountCode}, #{s.accountName}, #{s.accountType},
|
||||
#{s.debitTotal}, #{s.creditTotal}, #{s.balance})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<select id="selectSnapshotByCloseId" resultMap="SnapMap">
|
||||
SELECT snapshot_id, close_id, account_code, account_name, account_type,
|
||||
debit_total, credit_total, balance, created_at
|
||||
FROM account_balance_snapshot
|
||||
WHERE close_id = #{closeId}
|
||||
ORDER BY account_code
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?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.YearEndStatementMapper">
|
||||
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.income.YearEndStatementResp"/>
|
||||
<resultMap id="ExcelMap" type="com.ga.core.vo.income.YearEndStatementExcelVO"/>
|
||||
|
||||
<sql id="joinCols">
|
||||
yes.statement_id,
|
||||
yes.agent_id,
|
||||
a.agent_name,
|
||||
a.business_no,
|
||||
a.org_id,
|
||||
o.org_name,
|
||||
yes.statement_year,
|
||||
yes.statement_type,
|
||||
CASE yes.statement_type WHEN 'INCOME_DEDUCTION' THEN '사업소득 지급명세서' ELSE yes.statement_type END
|
||||
AS statement_type_name,
|
||||
yes.total_income,
|
||||
yes.total_withheld,
|
||||
yes.total_local_tax,
|
||||
yes.file_path,
|
||||
yes.file_format,
|
||||
yes.file_status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'YES_STATUS' AND cc.code = yes.file_status) AS file_status_name,
|
||||
yes.generated_at,
|
||||
yes.sent_at,
|
||||
yes.sent_to,
|
||||
yes.created_at,
|
||||
yes.updated_at
|
||||
</sql>
|
||||
|
||||
<sql id="joinFrom">
|
||||
FROM year_end_statement yes
|
||||
JOIN agent a ON a.agent_id = yes.agent_id
|
||||
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||
</sql>
|
||||
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
<include refid="joinFrom"/>
|
||||
<where>
|
||||
<if test="statementYear != null">AND yes.statement_year = #{statementYear}</if>
|
||||
<if test="agentId != null">AND yes.agent_id = #{agentId}</if>
|
||||
<if test="orgId != null">AND a.org_id = #{orgId}</if>
|
||||
<if test="fileStatus != null and fileStatus != ''">AND yes.file_status = #{fileStatus}</if>
|
||||
<if test="fileFormat != null and fileFormat != ''">AND yes.file_format = #{fileFormat}</if>
|
||||
<if test="searchKeyword != null and searchKeyword != ''">
|
||||
AND a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY yes.statement_year DESC, a.agent_id
|
||||
</select>
|
||||
|
||||
<select id="selectDetailById" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
<include refid="joinFrom"/>
|
||||
WHERE yes.statement_id = #{statementId}
|
||||
</select>
|
||||
|
||||
<select id="selectExcelCursor" resultMap="ExcelMap" fetchSize="500" resultSetType="FORWARD_ONLY">
|
||||
SELECT a.agent_name,
|
||||
o.org_name,
|
||||
a.business_no,
|
||||
yes.statement_year,
|
||||
yes.total_income,
|
||||
yes.total_withheld,
|
||||
yes.total_local_tax,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'YES_STATUS' AND cc.code = yes.file_status) AS file_status_name
|
||||
<include refid="joinFrom"/>
|
||||
<where>
|
||||
<if test="statementYear != null">AND yes.statement_year = #{statementYear}</if>
|
||||
<if test="agentId != null">AND yes.agent_id = #{agentId}</if>
|
||||
<if test="orgId != null">AND a.org_id = #{orgId}</if>
|
||||
<if test="fileStatus != null and fileStatus != ''">AND yes.file_status = #{fileStatus}</if>
|
||||
</where>
|
||||
ORDER BY a.agent_id
|
||||
</select>
|
||||
|
||||
<!--
|
||||
agent_annual_income(V63)를 기반으로 UPSERT.
|
||||
agent_annual_income 컬럼: agent_id / settle_year / total_income / total_withheld / generated_at
|
||||
지방소득세 = total_withheld * 0.1 (한국 표준)
|
||||
-->
|
||||
<insert id="upsertByYear">
|
||||
INSERT INTO year_end_statement
|
||||
(agent_id, statement_year, statement_type,
|
||||
total_income, total_withheld, total_local_tax, file_status, created_at)
|
||||
SELECT ai.agent_id,
|
||||
#{year},
|
||||
'INCOME_DEDUCTION',
|
||||
COALESCE(ai.total_commission, 0),
|
||||
COALESCE(ai.total_tax_withheld, 0),
|
||||
COALESCE(ai.total_local_tax, 0),
|
||||
'DRAFT',
|
||||
now()
|
||||
FROM agent_annual_income ai
|
||||
WHERE ai.settle_year = #{year}
|
||||
<if test="agentIds != null and agentIds.size() > 0">
|
||||
AND ai.agent_id IN
|
||||
<foreach collection="agentIds" item="aid" open="(" separator="," close=")">#{aid}</foreach>
|
||||
</if>
|
||||
ON CONFLICT (agent_id, statement_year, statement_type) DO UPDATE SET
|
||||
total_income = EXCLUDED.total_income,
|
||||
total_withheld = EXCLUDED.total_withheld,
|
||||
total_local_tax = EXCLUDED.total_local_tax,
|
||||
updated_at = now()
|
||||
</insert>
|
||||
|
||||
<update id="updateGenerated">
|
||||
UPDATE year_end_statement
|
||||
SET file_path = #{filePath},
|
||||
file_format = #{fileFormat},
|
||||
file_status = 'GENERATED',
|
||||
generated_at = now(),
|
||||
updated_at = now()
|
||||
WHERE statement_id = #{statementId}
|
||||
</update>
|
||||
|
||||
<update id="updateSent">
|
||||
UPDATE year_end_statement
|
||||
SET file_status = 'SENT',
|
||||
sent_at = now(),
|
||||
sent_to = #{sentTo},
|
||||
updated_at = now()
|
||||
WHERE statement_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
AND file_status = 'GENERATED'
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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.kpi.ForecastKpiMonthlyMapper">
|
||||
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.kpi.ForecastKpiMonthlyResp"/>
|
||||
<resultMap id="VOMap" type="com.ga.core.vo.kpi.ForecastKpiMonthlyVO"/>
|
||||
|
||||
<sql id="joinCols">
|
||||
f.forecast_id,
|
||||
f.forecast_month,
|
||||
f.target_org_id,
|
||||
(SELECT o.org_name FROM organization o WHERE o.org_id = f.target_org_id) AS target_org_name,
|
||||
f.metric_code,
|
||||
CASE f.metric_code
|
||||
WHEN 'MONTHLY_PREMIUM' THEN '월 수수료총액 (gross)'
|
||||
WHEN 'MONTHLY_PAYOUT' THEN '월 순지급액 (net)'
|
||||
WHEN 'RETENTION_13M' THEN '13개월 유지율'
|
||||
WHEN 'RETENTION_25M' THEN '25개월 유지율'
|
||||
ELSE f.metric_code
|
||||
END AS metric_name,
|
||||
f.forecast_value,
|
||||
f.lower_bound,
|
||||
f.upper_bound,
|
||||
f.method,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'FORECAST_METHOD' AND cc.code = f.method) AS method_name,
|
||||
f.base_period,
|
||||
f.sample_count,
|
||||
f.generated_at
|
||||
</sql>
|
||||
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
FROM forecast_kpi_monthly f
|
||||
<where>
|
||||
<if test="forecastMonth != null and forecastMonth != ''">AND f.forecast_month = #{forecastMonth}</if>
|
||||
<if test="targetOrgId != null">AND f.target_org_id = #{targetOrgId}</if>
|
||||
<if test="metricCode != null and metricCode != ''">AND f.metric_code = #{metricCode}</if>
|
||||
</where>
|
||||
ORDER BY f.forecast_month DESC, f.metric_code, f.target_org_id NULLS FIRST
|
||||
</select>
|
||||
|
||||
<!--
|
||||
UPSERT (forecast_month, COALESCE(target_org_id,-1), metric_code) 키.
|
||||
target_org_id NULL은 부분 UNIQUE 인덱스(uq_fkm_all)로 처리.
|
||||
ON CONFLICT 절은 두 가지 케이스 (전사/조직별)를 모두 다뤄야 하지만
|
||||
MyBatis foreach에서는 키별로 INSERT 직후 갱신을 보장하기 어려우니
|
||||
pre-DELETE 후 INSERT 패턴으로 단순화.
|
||||
-->
|
||||
<delete id="deleteByKey" parameterType="map">
|
||||
DELETE FROM forecast_kpi_monthly
|
||||
WHERE forecast_month = #{forecastMonth}
|
||||
AND metric_code = #{metricCode}
|
||||
AND ((#{targetOrgId} IS NULL AND target_org_id IS NULL)
|
||||
OR target_org_id = #{targetOrgId})
|
||||
</delete>
|
||||
|
||||
<insert id="upsertBatch">
|
||||
INSERT INTO forecast_kpi_monthly
|
||||
(forecast_month, target_org_id, metric_code,
|
||||
forecast_value, lower_bound, upper_bound,
|
||||
method, base_period, sample_count)
|
||||
VALUES
|
||||
<foreach collection="list" item="f" separator=",">
|
||||
(#{f.forecastMonth}, #{f.targetOrgId}, #{f.metricCode},
|
||||
#{f.forecastValue}, #{f.lowerBound}, #{f.upperBound},
|
||||
COALESCE(#{f.method}, 'SMA3'), #{f.basePeriod}, #{f.sampleCount})
|
||||
</foreach>
|
||||
ON CONFLICT DO NOTHING
|
||||
</insert>
|
||||
|
||||
<!--
|
||||
예측 산출.
|
||||
- sampleSize 개월 직전 평균(평균=forecast_value), 표준편차(±1σ=lower/upper)
|
||||
- method = SMA3/SMA6/NAIVE
|
||||
- metric:
|
||||
MONTHLY_PREMIUM = mv_monthly_summary.total_gross
|
||||
MONTHLY_PAYOUT = mv_monthly_summary.total_net
|
||||
RETENTION_13M = mv_retention.retention_rate_13m (carrier 평균)
|
||||
RETENTION_25M = mv_retention.retention_rate_25m (carrier 평균)
|
||||
- target_org_id 는 모두 NULL (전사) — 조직별은 후속 보강
|
||||
-->
|
||||
<select id="computeForecast" resultMap="VOMap">
|
||||
WITH params AS (
|
||||
SELECT REPLACE(#{forecastMonth},'-','') AS fmonth_yyyymm,
|
||||
TO_CHAR((TO_DATE(#{forecastMonth}||'-01','YYYY-MM-DD') - INTERVAL '1 months'), 'YYYYMM') AS prev_yyyymm,
|
||||
TO_CHAR((TO_DATE(#{forecastMonth}||'-01','YYYY-MM-DD') - (#{sampleSize}||' months')::INTERVAL), 'YYYYMM') AS from_yyyymm
|
||||
),
|
||||
mp AS (
|
||||
SELECT 'MONTHLY_PREMIUM' AS metric_code,
|
||||
AVG(total_gross) AS fv,
|
||||
AVG(total_gross) - COALESCE(STDDEV_POP(total_gross), 0) AS lb,
|
||||
AVG(total_gross) + COALESCE(STDDEV_POP(total_gross), 0) AS ub,
|
||||
COUNT(*) AS sc
|
||||
FROM mv_monthly_summary, params
|
||||
WHERE yyyymm BETWEEN from_yyyymm AND prev_yyyymm
|
||||
),
|
||||
mo AS (
|
||||
SELECT 'MONTHLY_PAYOUT' AS metric_code,
|
||||
AVG(total_net) AS fv,
|
||||
AVG(total_net) - COALESCE(STDDEV_POP(total_net), 0) AS lb,
|
||||
AVG(total_net) + COALESCE(STDDEV_POP(total_net), 0) AS ub,
|
||||
COUNT(*) AS sc
|
||||
FROM mv_monthly_summary, params
|
||||
WHERE yyyymm BETWEEN from_yyyymm AND prev_yyyymm
|
||||
),
|
||||
r13 AS (
|
||||
SELECT 'RETENTION_13M' AS metric_code,
|
||||
AVG(retention_rate_13m) AS fv,
|
||||
AVG(retention_rate_13m) - COALESCE(STDDEV_POP(retention_rate_13m), 0) AS lb,
|
||||
AVG(retention_rate_13m) + COALESCE(STDDEV_POP(retention_rate_13m), 0) AS ub,
|
||||
COUNT(*) AS sc
|
||||
FROM mv_retention, params
|
||||
WHERE contract_month BETWEEN from_yyyymm AND prev_yyyymm
|
||||
),
|
||||
r25 AS (
|
||||
SELECT 'RETENTION_25M' AS metric_code,
|
||||
AVG(retention_rate_25m) AS fv,
|
||||
AVG(retention_rate_25m) - COALESCE(STDDEV_POP(retention_rate_25m), 0) AS lb,
|
||||
AVG(retention_rate_25m) + COALESCE(STDDEV_POP(retention_rate_25m), 0) AS ub,
|
||||
COUNT(*) AS sc
|
||||
FROM mv_retention, params
|
||||
WHERE contract_month BETWEEN from_yyyymm AND prev_yyyymm
|
||||
),
|
||||
merged AS (
|
||||
SELECT * FROM mp WHERE mp.fv IS NOT NULL
|
||||
UNION ALL SELECT * FROM mo WHERE mo.fv IS NOT NULL
|
||||
UNION ALL SELECT * FROM r13 WHERE r13.fv IS NOT NULL
|
||||
UNION ALL SELECT * FROM r25 WHERE r25.fv IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
#{forecastMonth} AS forecast_month,
|
||||
NULL::BIGINT AS target_org_id,
|
||||
m.metric_code AS metric_code,
|
||||
m.fv AS forecast_value,
|
||||
m.lb AS lower_bound,
|
||||
m.ub AS upper_bound,
|
||||
#{method} AS method,
|
||||
CONCAT((SELECT from_yyyymm FROM params), '~', (SELECT prev_yyyymm FROM params)) AS base_period,
|
||||
m.sc::INT AS sample_count
|
||||
FROM merged m
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,155 @@
|
||||
<?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.kpi.RetentionAnomalyAlertMapper">
|
||||
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.kpi.RetentionAnomalyAlertResp"/>
|
||||
<resultMap id="VOMap" type="com.ga.core.vo.kpi.RetentionAnomalyAlertVO"/>
|
||||
|
||||
<sql id="joinCols">
|
||||
ra.alert_id,
|
||||
ra.target_type,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'ANOMALY_TARGET_TYPE' AND cc.code = ra.target_type) AS target_type_name,
|
||||
ra.target_id,
|
||||
CASE ra.target_type
|
||||
WHEN 'AGENT' THEN (SELECT a.agent_name FROM agent a WHERE a.agent_id = ra.target_id)
|
||||
WHEN 'ORG' THEN (SELECT o.org_name FROM organization o WHERE o.org_id = ra.target_id)
|
||||
ELSE '전사'
|
||||
END AS target_name,
|
||||
ra.alert_month,
|
||||
ra.retention_band,
|
||||
ra.expected_rate,
|
||||
ra.actual_rate,
|
||||
ra.deviation,
|
||||
ra.severity,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'ANOMALY_SEVERITY' AND cc.code = ra.severity) AS severity_name,
|
||||
ra.status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'ANOMALY_STATUS' AND cc.code = ra.status) AS status_name,
|
||||
ra.note,
|
||||
ra.detected_at,
|
||||
ra.reviewed_at,
|
||||
ra.reviewed_by,
|
||||
(SELECT u.user_name FROM users u WHERE u.user_id = ra.reviewed_by) AS reviewed_by_name
|
||||
</sql>
|
||||
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
FROM retention_anomaly_alert ra
|
||||
<where>
|
||||
<if test="alertMonth != null and alertMonth != ''">AND ra.alert_month = #{alertMonth}</if>
|
||||
<if test="targetType != null and targetType != ''">AND ra.target_type = #{targetType}</if>
|
||||
<if test="retentionBand != null and retentionBand != ''">AND ra.retention_band = #{retentionBand}</if>
|
||||
<if test="severity != null and severity != ''">AND ra.severity = #{severity}</if>
|
||||
<if test="status != null and status != ''">AND ra.status = #{status}</if>
|
||||
</where>
|
||||
ORDER BY ra.alert_month DESC, ra.severity DESC, ra.detected_at DESC
|
||||
</select>
|
||||
|
||||
<select id="selectDetailById" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
FROM retention_anomaly_alert ra
|
||||
WHERE ra.alert_id = #{alertId}
|
||||
</select>
|
||||
|
||||
<insert id="insertBatch">
|
||||
INSERT INTO retention_anomaly_alert
|
||||
(target_type, target_id, alert_month, retention_band,
|
||||
expected_rate, actual_rate, deviation, severity, status, note)
|
||||
VALUES
|
||||
<foreach collection="list" item="a" separator=",">
|
||||
(#{a.targetType}, #{a.targetId}, #{a.alertMonth}, #{a.retentionBand},
|
||||
#{a.expectedRate}, #{a.actualRate}, #{a.deviation},
|
||||
#{a.severity}, COALESCE(#{a.status}, 'NEW'), #{a.note})
|
||||
</foreach>
|
||||
ON CONFLICT DO NOTHING
|
||||
</insert>
|
||||
|
||||
<update id="updateStatus">
|
||||
UPDATE retention_anomaly_alert
|
||||
SET status = #{status},
|
||||
note = COALESCE(#{note}, note),
|
||||
reviewed_at = now(),
|
||||
reviewed_by = #{reviewedBy}
|
||||
WHERE alert_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</update>
|
||||
|
||||
<!--
|
||||
탐지 SQL.
|
||||
입력 alertMonth='YYYY-MM' → mv_retention.contract_month='YYYYMM'
|
||||
직전 3개월 평균 rate 대비 deviation >= 1.5%P → 알림.
|
||||
target_type='ALL' (carrier 통합) + carrier별 ('ORG'-like)는 carrier_id 를 target_id로.
|
||||
retention_band 두 종류(13M, 25M) UNION ALL.
|
||||
|
||||
severity 룰:
|
||||
|dev| < 3 → LOW
|
||||
|dev| < 6 → MED
|
||||
else → HIGH
|
||||
-->
|
||||
<select id="detectAnomalies" resultMap="VOMap">
|
||||
WITH curr AS (
|
||||
SELECT carrier_id, retention_rate_13m AS rate
|
||||
FROM mv_retention
|
||||
WHERE contract_month = REPLACE(#{alertMonth}, '-', '')
|
||||
),
|
||||
prev AS (
|
||||
SELECT carrier_id, AVG(retention_rate_13m) AS exp_rate
|
||||
FROM mv_retention
|
||||
WHERE contract_month < REPLACE(#{alertMonth}, '-', '')
|
||||
AND contract_month >=
|
||||
TO_CHAR((TO_DATE(#{alertMonth}||'-01','YYYY-MM-DD') - INTERVAL '3 months'), 'YYYYMM')
|
||||
GROUP BY carrier_id
|
||||
),
|
||||
anom13 AS (
|
||||
SELECT 'ALL'::VARCHAR(10) AS target_type,
|
||||
c.carrier_id AS target_id,
|
||||
#{alertMonth}::VARCHAR(7) AS alert_month,
|
||||
'13M'::VARCHAR(5) AS retention_band,
|
||||
p.exp_rate AS expected_rate,
|
||||
c.rate AS actual_rate,
|
||||
(c.rate - p.exp_rate) AS deviation,
|
||||
CASE
|
||||
WHEN ABS(c.rate - p.exp_rate) >= 6 THEN 'HIGH'
|
||||
WHEN ABS(c.rate - p.exp_rate) >= 3 THEN 'MED'
|
||||
ELSE 'LOW'
|
||||
END AS severity,
|
||||
'NEW'::VARCHAR(10) AS status,
|
||||
NULL::VARCHAR(300) AS note
|
||||
FROM curr c JOIN prev p ON p.carrier_id = c.carrier_id
|
||||
WHERE ABS(c.rate - p.exp_rate) >= 1.5
|
||||
),
|
||||
curr25 AS (
|
||||
SELECT carrier_id, retention_rate_25m AS rate
|
||||
FROM mv_retention
|
||||
WHERE contract_month = REPLACE(#{alertMonth}, '-', '')
|
||||
),
|
||||
prev25 AS (
|
||||
SELECT carrier_id, AVG(retention_rate_25m) AS exp_rate
|
||||
FROM mv_retention
|
||||
WHERE contract_month < REPLACE(#{alertMonth}, '-', '')
|
||||
AND contract_month >=
|
||||
TO_CHAR((TO_DATE(#{alertMonth}||'-01','YYYY-MM-DD') - INTERVAL '3 months'), 'YYYYMM')
|
||||
GROUP BY carrier_id
|
||||
),
|
||||
anom25 AS (
|
||||
SELECT 'ALL'::VARCHAR(10), c.carrier_id, #{alertMonth}::VARCHAR(7), '25M'::VARCHAR(5),
|
||||
p.exp_rate, c.rate, (c.rate - p.exp_rate),
|
||||
CASE
|
||||
WHEN ABS(c.rate - p.exp_rate) >= 6 THEN 'HIGH'
|
||||
WHEN ABS(c.rate - p.exp_rate) >= 3 THEN 'MED'
|
||||
ELSE 'LOW'
|
||||
END,
|
||||
'NEW'::VARCHAR(10),
|
||||
NULL::VARCHAR(300)
|
||||
FROM curr25 c JOIN prev25 p ON p.carrier_id = c.carrier_id
|
||||
WHERE ABS(c.rate - p.exp_rate) >= 1.5
|
||||
)
|
||||
SELECT * FROM anom13
|
||||
UNION ALL
|
||||
SELECT * FROM anom25
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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.tax.VatReportMapper">
|
||||
|
||||
<!-- 부가세 분기신고 응답 매핑 (조인 없음, 분기 집계 단위) -->
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.tax.VatReportResp"/>
|
||||
|
||||
<!-- 목록/상세 공통 SELECT 컬럼 -->
|
||||
<sql id="cols">
|
||||
r.vat_id,
|
||||
r.settle_quarter,
|
||||
r.total_sales,
|
||||
r.total_purchase,
|
||||
r.total_vat_payable,
|
||||
r.tax_invoice_count,
|
||||
r.generated_at,
|
||||
r.filed_at,
|
||||
r.file_status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'TAX_FILE_STATUS' AND cc.code = r.file_status) AS file_status_name,
|
||||
r.created_at,
|
||||
r.updated_at
|
||||
</sql>
|
||||
|
||||
<!-- ===== 목록 조회 ===== -->
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="cols"/>
|
||||
FROM vat_report r
|
||||
<where>
|
||||
<if test="settleQuarter != null and settleQuarter != ''">AND r.settle_quarter = #{settleQuarter}</if>
|
||||
<if test="fileStatus != null and fileStatus != ''">AND r.file_status = #{fileStatus}</if>
|
||||
</where>
|
||||
ORDER BY r.settle_quarter DESC
|
||||
</select>
|
||||
|
||||
<!-- ===== 단건 상세 ===== -->
|
||||
<select id="selectDetailById" resultMap="RespMap">
|
||||
SELECT <include refid="cols"/>
|
||||
FROM vat_report r
|
||||
WHERE r.vat_id = #{vatId}
|
||||
</select>
|
||||
|
||||
<!--
|
||||
분기 부가세 UPSERT 집계.
|
||||
tax_invoice 테이블에서 해당 분기 SUPPLY(매출)/PURCHASE(매입) SUM.
|
||||
- total_sales = SUM(supply_amount) WHERE invoice_type='SUPPLY'
|
||||
- total_purchase = SUM(supply_amount) WHERE invoice_type='PURCHASE'
|
||||
- total_vat_payable = total_sales * 0.1 - (SUM(tax_amount) WHERE PURCHASE)
|
||||
- tax_invoice_count = 건수 (취소 제외)
|
||||
분기 판단: issue_date 로 EXTRACT(YEAR/QUARTER) 사용.
|
||||
ON CONFLICT (settle_quarter) DO UPDATE.
|
||||
-->
|
||||
<insert id="upsertAggregate">
|
||||
INSERT INTO vat_report
|
||||
(settle_quarter,
|
||||
total_sales, total_purchase, total_vat_payable,
|
||||
tax_invoice_count,
|
||||
generated_at)
|
||||
SELECT
|
||||
#{quarter} AS settle_quarter,
|
||||
COALESCE(SUM(CASE WHEN ti.invoice_type = 'SUPPLY'
|
||||
THEN ti.supply_amount ELSE 0 END), 0) AS total_sales,
|
||||
COALESCE(SUM(CASE WHEN ti.invoice_type = 'PURCHASE'
|
||||
THEN ti.supply_amount ELSE 0 END), 0) AS total_purchase,
|
||||
COALESCE(SUM(CASE WHEN ti.invoice_type = 'SUPPLY'
|
||||
THEN ti.supply_amount ELSE 0 END), 0) * 0.1
|
||||
- COALESCE(SUM(CASE WHEN ti.invoice_type = 'PURCHASE'
|
||||
THEN ti.tax_amount ELSE 0 END), 0) AS total_vat_payable,
|
||||
COUNT(*) AS tax_invoice_count,
|
||||
NOW() AS generated_at
|
||||
FROM tax_invoice ti
|
||||
WHERE EXTRACT(YEAR FROM ti.issue_date) = CAST(SPLIT_PART(#{quarter}, '-Q', 1) AS INT)
|
||||
AND EXTRACT(QUARTER FROM ti.issue_date) = CAST(SPLIT_PART(#{quarter}, '-Q', 2) AS INT)
|
||||
AND ti.cancel_date IS NULL
|
||||
ON CONFLICT (settle_quarter) DO UPDATE SET
|
||||
total_sales = EXCLUDED.total_sales,
|
||||
total_purchase = EXCLUDED.total_purchase,
|
||||
total_vat_payable = EXCLUDED.total_vat_payable,
|
||||
tax_invoice_count = EXCLUDED.tax_invoice_count,
|
||||
generated_at = NOW(),
|
||||
updated_at = NOW()
|
||||
</insert>
|
||||
|
||||
<!-- ===== 신고 완료 처리 ===== -->
|
||||
<update id="updateFileStatusToFiled">
|
||||
UPDATE vat_report
|
||||
SET file_status = 'FILED',
|
||||
filed_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE vat_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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.tax.WithholdingTaxReportMapper">
|
||||
|
||||
<!-- agent, organization 조인 포함 응답 매핑 -->
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.tax.WithholdingTaxReportResp"/>
|
||||
|
||||
<!-- 목록/상세에서 공통으로 사용하는 SELECT 컬럼 -->
|
||||
<sql id="joinCols">
|
||||
r.report_id,
|
||||
r.agent_id,
|
||||
a.agent_name,
|
||||
a.org_id,
|
||||
o.org_name,
|
||||
r.settle_quarter,
|
||||
r.total_income,
|
||||
r.total_withheld,
|
||||
r.total_local_tax,
|
||||
r.generated_at,
|
||||
r.filed_at,
|
||||
r.file_status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'TAX_FILE_STATUS' AND cc.code = r.file_status) AS file_status_name,
|
||||
r.created_at,
|
||||
r.updated_at
|
||||
</sql>
|
||||
|
||||
<!-- ===== 목록 조회 ===== -->
|
||||
<select id="selectList" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
FROM withholding_tax_report r
|
||||
JOIN agent a ON a.agent_id = r.agent_id
|
||||
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||
<where>
|
||||
<if test="settleQuarter != null and settleQuarter != ''">AND r.settle_quarter = #{settleQuarter}</if>
|
||||
<if test="agentId != null">AND r.agent_id = #{agentId}</if>
|
||||
<if test="orgId != null">AND a.org_id = #{orgId}</if>
|
||||
<if test="fileStatus != null and fileStatus != ''">AND r.file_status = #{fileStatus}</if>
|
||||
<if test="searchKeyword != null and searchKeyword != ''">
|
||||
AND a.agent_name LIKE CONCAT('%', #{searchKeyword}, '%')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY r.settle_quarter DESC, r.agent_id ASC
|
||||
</select>
|
||||
|
||||
<!-- ===== 단건 상세 ===== -->
|
||||
<select id="selectDetailById" resultMap="RespMap">
|
||||
SELECT <include refid="joinCols"/>
|
||||
FROM withholding_tax_report r
|
||||
JOIN agent a ON a.agent_id = r.agent_id
|
||||
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||
WHERE r.report_id = #{reportId}
|
||||
</select>
|
||||
|
||||
<!--
|
||||
분기 원천세 UPSERT 집계.
|
||||
payment 테이블에서 해당 분기 설계사별 income_tax_amount / local_tax_amount / pay_amount SUM.
|
||||
분기 판단: pay_date 로 EXTRACT(YEAR/QUARTER) 사용. 분기 포맷 YYYY-Qn 파싱.
|
||||
예) '2025-Q1' → YEAR=2025, QUARTER=1
|
||||
ON CONFLICT (agent_id, settle_quarter) DO UPDATE.
|
||||
-->
|
||||
<insert id="upsertQuarterlyAggregate">
|
||||
INSERT INTO withholding_tax_report
|
||||
(agent_id, settle_quarter,
|
||||
total_income, total_withheld, total_local_tax,
|
||||
generated_at)
|
||||
SELECT
|
||||
p.agent_id,
|
||||
#{quarter} AS settle_quarter,
|
||||
COALESCE(SUM(p.pay_amount), 0) AS total_income,
|
||||
COALESCE(SUM(p.income_tax_amount), 0) AS total_withheld,
|
||||
COALESCE(SUM(p.local_tax_amount), 0) AS total_local_tax,
|
||||
NOW() AS generated_at
|
||||
FROM payment p
|
||||
WHERE EXTRACT(YEAR FROM p.pay_date) = CAST(SPLIT_PART(#{quarter}, '-Q', 1) AS INT)
|
||||
AND EXTRACT(QUARTER FROM p.pay_date) = CAST(SPLIT_PART(#{quarter}, '-Q', 2) AS INT)
|
||||
GROUP BY p.agent_id
|
||||
ON CONFLICT (agent_id, settle_quarter) DO UPDATE SET
|
||||
total_income = EXCLUDED.total_income,
|
||||
total_withheld = EXCLUDED.total_withheld,
|
||||
total_local_tax = EXCLUDED.total_local_tax,
|
||||
generated_at = NOW(),
|
||||
updated_at = NOW()
|
||||
</insert>
|
||||
|
||||
<!-- ===== 신고 완료 처리 ===== -->
|
||||
<update id="updateFileStatusToFiled">
|
||||
UPDATE withholding_tax_report
|
||||
SET file_status = 'FILED',
|
||||
filed_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE report_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user