feat: 수수료 공식 갭 보완 + 8종 신규모듈 풀스택 (V116~V123)
수수료 공식 갭분석(5 병렬감사) 후 확정 버그 3건 수정 + 개념적으로 빠진 모듈 8종 풀스택 구현. [확정 버그 3건] - 1200% 누적한도(checkContractTotal)를 배치 CalcRecruitStep에 연결(기존 1차년 한도만 적용 → 두 한도 중 더 제한적인 값으로 clip+이연). +CalcRecruitStepTest - BatchInstallmentPlanGenerator planMonth 반영(하드코딩 1 제거, API판과 동일). +단위테스트 - PersistencyBonus 임계유지율 게이트 + FIXED 정액 룰 도출. +PersistencyBonusServiceTest [신규 모듈 8종] - MOD-1 도입수수료(recruiter/development) / MOD-2 선지급·차익정산 / MOD-3 환수 분할상환 - MOD-4 익월부활 재지급 / MOD-5 정산대사 입수 / MOD-6 환수 시효 / MOD-7 환수 감면 - MOD-8 채널별 수수료차등(sales_channel) 각 DB(테이블+메뉴+권한+공통코드)→VO/Mapper/Enum→Service/Controller→화면 풀스택. 배치 정산연계: AggregateStep에 도입/익월부활 가산(11종), 선지급 FIFO 상계(멱등 reverse), MOD-8 채널 우선 요율조회(NULL 폴백 하위호환). [검증] - 전체 ./gradlew build(전 모듈+전 테스트) GREEN - Flyway V116~V123 운영DB 적용(schema v115→v123) - 라이브 스모크 8 GET=200, MOD-1 RATE공식 50000 실측, 액션 POST 5xx 0건 - 적대적 코드리뷰 APPROVED(MOD-8 계산기 미연결 버그 발견·수정) 스펙: docs/DOMAIN_GAP_MOD1-8.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.AdvancePaymentService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.AdvancePaymentResp;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSaveReq;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "선지급정산")
|
||||
@RestController
|
||||
@RequestMapping("/api/advance-payments")
|
||||
@RequiredArgsConstructor
|
||||
public class AdvancePaymentController {
|
||||
|
||||
private final AdvancePaymentService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<AdvancePaymentResp>> list(
|
||||
@ModelAttribute AdvancePaymentSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{advanceId}")
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||
public ApiResponse<AdvancePaymentResp> detail(@PathVariable Long advanceId) {
|
||||
return ApiResponse.ok(service.detail(advanceId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody AdvancePaymentSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{advanceId}/waive")
|
||||
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||
public ApiResponse<Void> waive(@PathVariable Long advanceId) {
|
||||
service.waive(advanceId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.CarrierReconciliationInService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.CarrierReconciliationInResp;
|
||||
import com.ga.core.vo.commission.CarrierReconciliationInSaveReq;
|
||||
import com.ga.core.vo.commission.CarrierReconciliationInSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "정산대사입수")
|
||||
@RestController
|
||||
@RequestMapping("/api/carrier-recon-in")
|
||||
@RequiredArgsConstructor
|
||||
public class CarrierReconciliationInController {
|
||||
|
||||
private final CarrierReconciliationInService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||
public ApiResponse<PageResponse<CarrierReconciliationInResp>> list(
|
||||
@ModelAttribute CarrierReconciliationInSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{reconId}")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||
public ApiResponse<CarrierReconciliationInResp> detail(@PathVariable Long reconId) {
|
||||
return ApiResponse.ok(service.detail(reconId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody CarrierReconciliationInSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{reconId}/reconcile")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Void> reconcile(@PathVariable Long reconId) {
|
||||
service.reconcile(reconId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{reconId}/approve")
|
||||
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||
public ApiResponse<Void> approve(@PathVariable Long reconId) {
|
||||
service.approve(reconId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ChargebackWaiverService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.ChargebackWaiverRequestResp;
|
||||
import com.ga.core.vo.commission.ChargebackWaiverRequestSaveReq;
|
||||
import com.ga.core.vo.commission.ChargebackWaiverRequestSearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "환수감면신청")
|
||||
@RestController
|
||||
@RequestMapping("/api/chargeback-waivers")
|
||||
@RequiredArgsConstructor
|
||||
public class ChargebackWaiverController {
|
||||
|
||||
private final ChargebackWaiverService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||
public ApiResponse<PageResponse<ChargebackWaiverRequestResp>> list(
|
||||
@ModelAttribute ChargebackWaiverRequestSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{requestId}")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||
public ApiResponse<ChargebackWaiverRequestResp> detail(@PathVariable Long requestId) {
|
||||
return ApiResponse.ok(service.detail(requestId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody ChargebackWaiverRequestSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{requestId}/approve")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Void> approve(@PathVariable Long requestId) {
|
||||
service.approve(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{requestId}/reject")
|
||||
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||
public ApiResponse<Void> reject(@PathVariable Long requestId) {
|
||||
service.reject(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.ClawbackInstallmentService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.ClawbackInstallmentPlanResp;
|
||||
import com.ga.core.vo.commission.ClawbackInstallmentPlanSearchParam;
|
||||
import com.ga.core.vo.commission.ClawbackInstallmentPlanVO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "환수분할상환")
|
||||
@RestController
|
||||
@RequestMapping("/api/clawback-installments")
|
||||
@RequiredArgsConstructor
|
||||
public class ClawbackInstallmentController {
|
||||
|
||||
private final ClawbackInstallmentService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<ClawbackInstallmentPlanResp>> list(
|
||||
@ModelAttribute ClawbackInstallmentPlanSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{planId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<ClawbackInstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||
return ApiResponse.ok(service.detail(planId));
|
||||
}
|
||||
|
||||
@GetMapping("/by-receivable/{receivableId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<List<ClawbackInstallmentPlanVO>> listByReceivable(
|
||||
@PathVariable Long receivableId) {
|
||||
return ApiResponse.ok(service.listByReceivable(receivableId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할상환 계획 생성.
|
||||
* Request body: { "count": N, "baseMonth": "YYYYMM" }
|
||||
*/
|
||||
@PostMapping("/generate/{receivableId}")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||
public ApiResponse<Void> generate(@PathVariable Long receivableId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
int count = Integer.parseInt(body.get("count").toString());
|
||||
String baseMonth = body.get("baseMonth").toString();
|
||||
service.generate(receivableId, count, baseMonth);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{planId}/cancel")
|
||||
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long planId) {
|
||||
service.cancel(planId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.LapsedReinstatementService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.LapsedReinstatementRecoveryResp;
|
||||
import com.ga.core.vo.commission.LapsedReinstatementRecoverySaveReq;
|
||||
import com.ga.core.vo.commission.LapsedReinstatementRecoverySearchParam;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "익월부활재지급")
|
||||
@RestController
|
||||
@RequestMapping("/api/lapsed-reinstatements")
|
||||
@RequiredArgsConstructor
|
||||
public class LapsedReinstatementController {
|
||||
|
||||
private final LapsedReinstatementService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<LapsedReinstatementRecoveryResp>> list(
|
||||
@ModelAttribute LapsedReinstatementRecoverySearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{recoveryId}")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||
public ApiResponse<LapsedReinstatementRecoveryResp> detail(@PathVariable Long recoveryId) {
|
||||
return ApiResponse.ok(service.detail(recoveryId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody LapsedReinstatementRecoverySaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{recoveryId}/repay")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Void> repay(@PathVariable Long recoveryId) {
|
||||
service.repay(recoveryId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{recoveryId}/cancel")
|
||||
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long recoveryId) {
|
||||
service.cancel(recoveryId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.RecruiterCommissionService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "도입수수료")
|
||||
@RestController
|
||||
@RequestMapping("/api/recruiter-commissions")
|
||||
@RequiredArgsConstructor
|
||||
public class RecruiterCommissionController {
|
||||
|
||||
private final RecruiterCommissionService service;
|
||||
|
||||
// ── Rule ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/rules")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RecruiterCommissionRuleResp>> listRules(
|
||||
@ModelAttribute RecruiterCommissionRuleSearchParam param) {
|
||||
return ApiResponse.ok(service.listRules(param));
|
||||
}
|
||||
|
||||
@GetMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<RecruiterCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||
return ApiResponse.ok(service.detailRule(ruleId));
|
||||
}
|
||||
|
||||
@PostMapping("/rules")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Long> createRule(@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||
return ApiResponse.ok(service.createRule(req));
|
||||
}
|
||||
|
||||
@PutMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||
@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||
service.updateRule(ruleId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping("/rules/{ruleId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "DELETE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||
service.deleteRule(ruleId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ── Ledger ────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<PageResponse<RecruiterCommissionLedgerResp>> listLedgers(
|
||||
@ModelAttribute RecruiterCommissionLedgerSearchParam param) {
|
||||
return ApiResponse.ok(service.listLedgers(param));
|
||||
}
|
||||
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||
public ApiResponse<RecruiterCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||
}
|
||||
|
||||
@PostMapping("/ledgers")
|
||||
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_ledger")
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody RecruiterCommissionLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import com.ga.api.service.commission.StatuteService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||
import com.ga.core.vo.commission.StatuteConfigSaveReq;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "환수시효관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/statute")
|
||||
@RequiredArgsConstructor
|
||||
public class StatuteController {
|
||||
|
||||
private final StatuteService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||
public ApiResponse<List<StatuteConfigResp>> list() {
|
||||
return ApiResponse.ok(service.listConfigs());
|
||||
}
|
||||
|
||||
@GetMapping("/{configId}")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||
public ApiResponse<StatuteConfigResp> detail(@PathVariable Long configId) {
|
||||
return ApiResponse.ok(service.detailConfig(configId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "CREATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||
return ApiResponse.ok(service.createConfig(req));
|
||||
}
|
||||
|
||||
@PutMapping("/{configId}")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||
public ApiResponse<Void> update(@PathVariable Long configId,
|
||||
@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||
service.updateConfig(configId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료 대상 환수채권 일괄 대손 처리.
|
||||
* Request body: { "baseMonth": "YYYYMM" }
|
||||
*/
|
||||
@PostMapping("/expire")
|
||||
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "STATUTE_CONFIG", table = "clawback_receivable")
|
||||
public ApiResponse<Integer> expire(@RequestBody Map<String, String> body) {
|
||||
String baseMonth = body.get("baseMonth");
|
||||
if (baseMonth == null || baseMonth.isBlank()) {
|
||||
baseMonth = com.ga.common.util.DateUtil.getSettleMonth();
|
||||
}
|
||||
return ApiResponse.ok(service.expireOverdue(baseMonth));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.commission.AdvancePaymentMapper;
|
||||
import com.ga.core.vo.commission.AdvancePaymentResp;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSaveReq;
|
||||
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||
import com.ga.core.vo.commission.AdvancePaymentVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class AdvancePaymentService {
|
||||
|
||||
private final AdvancePaymentMapper mapper;
|
||||
|
||||
public PageResponse<AdvancePaymentResp> list(AdvancePaymentSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
public AdvancePaymentResp detail(Long advanceId) {
|
||||
AdvancePaymentResp resp = mapper.selectById(advanceId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 선지급 등록 — balance = principal, status = OUTSTANDING.
|
||||
*/
|
||||
@Transactional
|
||||
public Long create(AdvancePaymentSaveReq req) {
|
||||
AdvancePaymentVO vo = new AdvancePaymentVO();
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setAdvanceMonth(req.getAdvanceMonth());
|
||||
vo.setPrincipalAmount(req.getPrincipalAmount());
|
||||
vo.setRecoveredAmount(BigDecimal.ZERO);
|
||||
vo.setBalance(req.getPrincipalAmount());
|
||||
vo.setStatus("OUTSTANDING");
|
||||
vo.setMemo(req.getMemo());
|
||||
mapper.insert(vo);
|
||||
return vo.getAdvanceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 선지급 면제 — status = WAIVED, balance = 0.
|
||||
*/
|
||||
@Transactional
|
||||
public void waive(Long advanceId) {
|
||||
AdvancePaymentResp resp = mapper.selectById(advanceId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if ("CLEARED".equals(resp.getStatus()) || "WAIVED".equals(resp.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
AdvancePaymentVO vo = new AdvancePaymentVO();
|
||||
vo.setAdvanceId(advanceId);
|
||||
vo.setAgentId(resp.getAgentId());
|
||||
vo.setAdvanceMonth(resp.getAdvanceMonth());
|
||||
vo.setPrincipalAmount(resp.getPrincipalAmount());
|
||||
vo.setRecoveredAmount(resp.getRecoveredAmount());
|
||||
vo.setBalance(BigDecimal.ZERO);
|
||||
vo.setStatus("WAIVED");
|
||||
vo.setMemo(resp.getMemo());
|
||||
mapper.update(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 설계사의 미상환 잔액 합계 (OUTSTANDING + PARTIAL).
|
||||
*/
|
||||
public BigDecimal outstandingByAgent(Long agentId) {
|
||||
BigDecimal total = mapper.outstandingByAgent(agentId);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.common.util.SecurityUtil;
|
||||
import com.ga.core.mapper.commission.CarrierReconciliationInMapper;
|
||||
import com.ga.core.mapper.commission.IncomeCommissionLedgerMapper;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class CarrierReconciliationInService {
|
||||
|
||||
private final CarrierReconciliationInMapper mapper;
|
||||
private final IncomeCommissionLedgerMapper incomeMapper;
|
||||
|
||||
public PageResponse<CarrierReconciliationInResp> list(CarrierReconciliationInSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
public CarrierReconciliationInResp detail(Long reconId) {
|
||||
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대사 등록 — reported_total 입력, status = RECEIVED.
|
||||
*/
|
||||
@Transactional
|
||||
public Long create(CarrierReconciliationInSaveReq req) {
|
||||
CarrierReconciliationInVO vo = new CarrierReconciliationInVO();
|
||||
vo.setCompanyId(req.getCompanyId());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setFileName(req.getFileName());
|
||||
vo.setRecordCount(req.getRecordCount() != null ? req.getRecordCount() : 0);
|
||||
vo.setReportedTotal(req.getReportedTotal());
|
||||
vo.setSystemTotal(BigDecimal.ZERO);
|
||||
vo.setVarianceAmount(BigDecimal.ZERO);
|
||||
vo.setStatus("RECEIVED");
|
||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||
mapper.insert(vo);
|
||||
return vo.getReconId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 대사 실행 — system_total = income_commission 집계, variance = reported − system.
|
||||
* |variance| < 1 이면 MATCHED, 아니면 VARIANCE.
|
||||
*/
|
||||
@Transactional
|
||||
public void reconcile(Long reconId) {
|
||||
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if ("APPROVED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
|
||||
BigDecimal systemTotal = incomeMapper.sumByCarrierAndMonth(
|
||||
resp.getCompanyId(), resp.getSettleMonth());
|
||||
if (systemTotal == null) systemTotal = BigDecimal.ZERO;
|
||||
|
||||
BigDecimal variance = resp.getReportedTotal().subtract(systemTotal);
|
||||
String newStatus = variance.abs().compareTo(BigDecimal.ONE) < 0 ? "MATCHED" : "VARIANCE";
|
||||
|
||||
CarrierReconciliationInVO vo = buildVO(resp);
|
||||
vo.setSystemTotal(systemTotal);
|
||||
vo.setVarianceAmount(variance);
|
||||
vo.setStatus(newStatus);
|
||||
mapper.update(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 처리 — status = APPROVED.
|
||||
*/
|
||||
@Transactional
|
||||
public void approve(Long reconId) {
|
||||
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if ("RECEIVED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
CarrierReconciliationInVO vo = buildVO(resp);
|
||||
vo.setStatus("APPROVED");
|
||||
mapper.update(vo);
|
||||
}
|
||||
|
||||
private CarrierReconciliationInVO buildVO(CarrierReconciliationInResp resp) {
|
||||
CarrierReconciliationInVO vo = new CarrierReconciliationInVO();
|
||||
vo.setReconId(resp.getReconId());
|
||||
vo.setCompanyId(resp.getCompanyId());
|
||||
vo.setSettleMonth(resp.getSettleMonth());
|
||||
vo.setFileName(resp.getFileName());
|
||||
vo.setRecordCount(resp.getRecordCount());
|
||||
vo.setReportedTotal(resp.getReportedTotal());
|
||||
vo.setSystemTotal(resp.getSystemTotal());
|
||||
vo.setVarianceAmount(resp.getVarianceAmount());
|
||||
vo.setStatus(resp.getStatus());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.common.util.SecurityUtil;
|
||||
import com.ga.core.mapper.commission.ChargebackWaiverRequestMapper;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class ChargebackWaiverService {
|
||||
|
||||
private final ChargebackWaiverRequestMapper mapper;
|
||||
|
||||
public PageResponse<ChargebackWaiverRequestResp> list(ChargebackWaiverRequestSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
public ChargebackWaiverRequestResp detail(Long requestId) {
|
||||
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 감면 신청 등록 — waiver_amount <= original_chargeback 검증.
|
||||
*/
|
||||
@Transactional
|
||||
public Long create(ChargebackWaiverRequestSaveReq req) {
|
||||
if (req.getWaiverAmount().compareTo(req.getOriginalChargeback()) > 0) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
ChargebackWaiverRequestVO vo = new ChargebackWaiverRequestVO();
|
||||
vo.setContractId(req.getContractId());
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setOriginalChargeback(req.getOriginalChargeback());
|
||||
vo.setWaiverAmount(req.getWaiverAmount());
|
||||
vo.setReason(req.getReason());
|
||||
vo.setApprovalStatus("REQUESTED");
|
||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||
mapper.insert(vo);
|
||||
return vo.getRequestId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 처리 — approval_status = APPROVED, approver_id = 현재 사용자, approved_at = now.
|
||||
*/
|
||||
@Transactional
|
||||
public void approve(Long requestId) {
|
||||
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!"REQUESTED".equals(resp.getApprovalStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
mapper.updateApproval(requestId, "APPROVED", SecurityUtil.getCurrentUserId(), LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려 처리 — approval_status = REJECTED.
|
||||
*/
|
||||
@Transactional
|
||||
public void reject(Long requestId) {
|
||||
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!"REQUESTED".equals(resp.getApprovalStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
mapper.updateApproval(requestId, "REJECTED", SecurityUtil.getCurrentUserId(), LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.common.util.DateUtil;
|
||||
import com.ga.core.mapper.commission.ClawbackInstallmentPlanMapper;
|
||||
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class ClawbackInstallmentService {
|
||||
|
||||
private final ClawbackInstallmentPlanMapper planMapper;
|
||||
private final ClawbackReceivableMapper receivableMapper;
|
||||
|
||||
public PageResponse<ClawbackInstallmentPlanResp> list(ClawbackInstallmentPlanSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(planMapper.selectList(param));
|
||||
}
|
||||
|
||||
public ClawbackInstallmentPlanResp detail(Long planId) {
|
||||
ClawbackInstallmentPlanResp resp = planMapper.selectById(planId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
public List<ClawbackInstallmentPlanVO> listByReceivable(Long receivableId) {
|
||||
return planMapper.selectByReceivable(receivableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 환수채권을 N회차로 균등 분할상환 계획 생성.
|
||||
* 마지막 회차는 잔차(총액 - 앞 회차 합계) 흡수.
|
||||
* 기준월: baseMonth(YYYYMM), due_month = baseMonth + (회차-1)
|
||||
*/
|
||||
@Transactional
|
||||
public void generate(Long receivableId, int count, String baseMonth) {
|
||||
if (count < 1) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
|
||||
// 이미 SCHEDULED 계획이 있으면 중복 방지
|
||||
List<ClawbackInstallmentPlanVO> existing = planMapper.selectByReceivable(receivableId);
|
||||
boolean hasScheduled = existing.stream()
|
||||
.anyMatch(p -> "SCHEDULED".equals(p.getStatus()));
|
||||
if (hasScheduled) throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||
|
||||
ClawbackReceivableResp receivableResp = receivableMapper.selectById(receivableId);
|
||||
if (receivableResp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
|
||||
BigDecimal total = receivableResp.getBalance();
|
||||
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0)
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
|
||||
BigDecimal unit = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN);
|
||||
BigDecimal sumBefore = unit.multiply(BigDecimal.valueOf(count - 1));
|
||||
BigDecimal last = total.subtract(sumBefore);
|
||||
|
||||
List<ClawbackInstallmentPlanVO> plans = new ArrayList<>();
|
||||
for (int i = 1; i <= count; i++) {
|
||||
ClawbackInstallmentPlanVO vo = new ClawbackInstallmentPlanVO();
|
||||
vo.setReceivableId(receivableId);
|
||||
vo.setInstallmentNo(i);
|
||||
vo.setInstallmentAmount(i == count ? last : unit);
|
||||
vo.setDueMonth(DateUtil.addMonth(baseMonth, i - 1));
|
||||
vo.setStatus("SCHEDULED");
|
||||
plans.add(vo);
|
||||
}
|
||||
planMapper.insertBatch(plans);
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할상환 계획 취소 (SCHEDULED → CANCELLED).
|
||||
*/
|
||||
@Transactional
|
||||
public void cancel(Long planId) {
|
||||
ClawbackInstallmentPlanResp resp = planMapper.selectById(planId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!"SCHEDULED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
planMapper.updateStatus(planId, "CANCELLED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.commission.LapsedReinstatementRecoveryMapper;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class LapsedReinstatementService {
|
||||
|
||||
private final LapsedReinstatementRecoveryMapper mapper;
|
||||
|
||||
public PageResponse<LapsedReinstatementRecoveryResp> list(LapsedReinstatementRecoverySearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
public LapsedReinstatementRecoveryResp detail(Long recoveryId) {
|
||||
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부활 등록 — withheld_amount 입력, status = PENDING.
|
||||
*/
|
||||
@Transactional
|
||||
public Long create(LapsedReinstatementRecoverySaveReq req) {
|
||||
LapsedReinstatementRecoveryVO vo = new LapsedReinstatementRecoveryVO();
|
||||
vo.setContractId(req.getContractId());
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setLapsedMonth(req.getLapsedMonth());
|
||||
vo.setReinstatedMonth(req.getReinstatedMonth());
|
||||
vo.setWithheldAmount(req.getWithheldAmount());
|
||||
vo.setRepaidAmount(java.math.BigDecimal.ZERO);
|
||||
vo.setStatus("PENDING");
|
||||
mapper.insert(vo);
|
||||
return vo.getRecoveryId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 재지급 처리 — status = REPAID, repaid_amount = withheld_amount.
|
||||
*/
|
||||
@Transactional
|
||||
public void repay(Long recoveryId) {
|
||||
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!"PENDING".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
mapper.updateStatus(recoveryId, "REPAID", resp.getWithheldAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 — status = CANCELLED.
|
||||
*/
|
||||
@Transactional
|
||||
public void cancel(Long recoveryId) {
|
||||
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if ("REPAID".equals(resp.getStatus()) || "CANCELLED".equals(resp.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||
}
|
||||
mapper.updateStatus(recoveryId, "CANCELLED", java.math.BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
@@ -86,6 +89,24 @@ public class PersistencyBonusService {
|
||||
*/
|
||||
@Transactional
|
||||
public void upsertBonus(PersistencyBonusSaveReq req) {
|
||||
BigDecimal bonusAmount = req.getBonusAmount();
|
||||
|
||||
// 지속수당 지급 게이트: 룰이 지정되면 유지율이 임계(retention_rate_threshold) 이상이어야 지급.
|
||||
// FIXED형 룰이면 지급액을 룰의 bonus_value(정액)로 도출한다.
|
||||
if (req.getRuleId() != null) {
|
||||
PersistencyBonusRuleResp rule = ruleMapper.selectById(req.getRuleId());
|
||||
if (rule == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (rule.getRetentionRateThreshold() != null
|
||||
&& (req.getRetentionRate() == null
|
||||
|| req.getRetentionRate().compareTo(rule.getRetentionRateThreshold()) < 0)) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER,
|
||||
"유지율이 지급 임계(" + rule.getRetentionRateThreshold() + ")에 미달합니다.");
|
||||
}
|
||||
if ("FIXED".equals(rule.getBonusType()) && rule.getBonusValue() != null) {
|
||||
bonusAmount = rule.getBonusValue().setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
PersistencyBonusVO vo = new PersistencyBonusVO();
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setRuleId(req.getRuleId());
|
||||
@@ -93,7 +114,7 @@ public class PersistencyBonusService {
|
||||
vo.setRetentionMonth(req.getRetentionMonth());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setRetentionRate(req.getRetentionRate());
|
||||
vo.setBonusAmount(req.getBonusAmount());
|
||||
vo.setBonusAmount(bonusAmount);
|
||||
vo.setStatus("CALCULATED");
|
||||
bonusMapper.upsert(vo);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper;
|
||||
import com.ga.core.mapper.commission.RecruiterCommissionRuleMapper;
|
||||
import com.ga.core.vo.commission.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class RecruiterCommissionService {
|
||||
|
||||
private final RecruiterCommissionRuleMapper ruleMapper;
|
||||
private final RecruiterCommissionLedgerMapper ledgerMapper;
|
||||
|
||||
// ── Rule ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public PageResponse<RecruiterCommissionRuleResp> listRules(RecruiterCommissionRuleSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(ruleMapper.selectList(param));
|
||||
}
|
||||
|
||||
public RecruiterCommissionRuleResp detailRule(Long ruleId) {
|
||||
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long createRule(RecruiterCommissionRuleSaveReq req) {
|
||||
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||
vo.setRecruiterType(req.getRecruiterType());
|
||||
vo.setCalcType(req.getCalcType());
|
||||
vo.setCommissionRate(req.getCommissionRate());
|
||||
vo.setFixedAmount(req.getFixedAmount());
|
||||
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||
vo.setEffectiveTo(req.getEffectiveTo());
|
||||
ruleMapper.insert(vo);
|
||||
return vo.getRuleId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateRule(Long ruleId, RecruiterCommissionRuleSaveReq req) {
|
||||
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||
vo.setRuleId(ruleId);
|
||||
vo.setRecruiterType(req.getRecruiterType());
|
||||
vo.setCalcType(req.getCalcType());
|
||||
vo.setCommissionRate(req.getCommissionRate());
|
||||
vo.setFixedAmount(req.getFixedAmount());
|
||||
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||
vo.setEffectiveTo(req.getEffectiveTo());
|
||||
ruleMapper.update(vo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteRule(Long ruleId) {
|
||||
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
ruleMapper.delete(ruleId);
|
||||
}
|
||||
|
||||
// ── Ledger ────────────────────────────────────────────────────────────────
|
||||
|
||||
public PageResponse<RecruiterCommissionLedgerResp> listLedgers(RecruiterCommissionLedgerSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(ledgerMapper.selectList(param));
|
||||
}
|
||||
|
||||
public RecruiterCommissionLedgerResp detailLedger(Long ledgerId) {
|
||||
RecruiterCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 도입수수료 원장 생성.
|
||||
* 활성 룰 목록 중 recruiterType + 오늘 기준 유효한 룰 우선 적용:
|
||||
* RATE → commissionAmount = baseAmount × commissionRate
|
||||
* FIXED → commissionAmount = fixedAmount
|
||||
* 룰 없음 → 요청 commissionAmount 사용 (수동 등록 호환).
|
||||
*/
|
||||
@Transactional
|
||||
public Long createLedger(RecruiterCommissionLedgerSaveReq req) {
|
||||
RecruiterCommissionRuleVO activeRule = findActiveRule(req.getRecruiterType());
|
||||
|
||||
BigDecimal commissionAmount;
|
||||
if (activeRule != null) {
|
||||
if ("FIXED".equals(activeRule.getCalcType())) {
|
||||
if (activeRule.getFixedAmount() == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
commissionAmount = activeRule.getFixedAmount().setScale(2, RoundingMode.HALF_UP);
|
||||
} else { // RATE
|
||||
BigDecimal rate = activeRule.getCommissionRate();
|
||||
if (rate == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
commissionAmount = req.getBaseAmount().multiply(rate).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
} else {
|
||||
commissionAmount = req.getCommissionAmount();
|
||||
if (commissionAmount == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
RecruiterCommissionLedgerVO vo = new RecruiterCommissionLedgerVO();
|
||||
vo.setRecruiterAgentId(req.getRecruiterAgentId());
|
||||
vo.setRecruitAgentId(req.getRecruitAgentId());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setRecruiterType(req.getRecruiterType());
|
||||
vo.setBaseAmount(req.getBaseAmount());
|
||||
vo.setCommissionAmount(commissionAmount);
|
||||
vo.setStatus("CALCULATED");
|
||||
ledgerMapper.insert(vo);
|
||||
return vo.getLedgerId();
|
||||
}
|
||||
|
||||
/** recruiterType + 오늘 날짜 기준 유효 룰 첫 번째 반환. */
|
||||
private RecruiterCommissionRuleVO findActiveRule(String recruiterType) {
|
||||
RecruiterCommissionRuleSearchParam param = new RecruiterCommissionRuleSearchParam();
|
||||
param.setRecruiterType(recruiterType);
|
||||
List<RecruiterCommissionRuleResp> list = ruleMapper.selectList(param);
|
||||
java.time.LocalDate today = java.time.LocalDate.now();
|
||||
return list.stream()
|
||||
.filter(r -> !today.isBefore(r.getEffectiveFrom())
|
||||
&& (r.getEffectiveTo() == null || !today.isAfter(r.getEffectiveTo())))
|
||||
.findFirst()
|
||||
.map(r -> {
|
||||
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||
vo.setRuleId(r.getRuleId());
|
||||
vo.setRecruiterType(r.getRecruiterType());
|
||||
vo.setCalcType(r.getCalcType());
|
||||
vo.setCommissionRate(r.getCommissionRate());
|
||||
vo.setFixedAmount(r.getFixedAmount());
|
||||
vo.setEffectiveFrom(r.getEffectiveFrom());
|
||||
vo.setEffectiveTo(r.getEffectiveTo());
|
||||
return vo;
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ga.api.service.commission;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.util.DateUtil;
|
||||
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||
import com.ga.core.mapper.commission.StatuteConfigMapper;
|
||||
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||
import com.ga.core.vo.commission.StatuteConfigSaveReq;
|
||||
import com.ga.core.vo.commission.StatuteConfigVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class StatuteService {
|
||||
|
||||
private final StatuteConfigMapper configMapper;
|
||||
private final ClawbackReceivableMapper receivableMapper;
|
||||
|
||||
public List<StatuteConfigResp> listConfigs() {
|
||||
return configMapper.selectList();
|
||||
}
|
||||
|
||||
public StatuteConfigResp detailConfig(Long configId) {
|
||||
StatuteConfigResp resp = configMapper.selectById(configId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long createConfig(StatuteConfigSaveReq req) {
|
||||
StatuteConfigVO vo = new StatuteConfigVO();
|
||||
vo.setEntityType(req.getEntityType());
|
||||
vo.setExpirationMonths(req.getExpirationMonths());
|
||||
vo.setIsActive(req.getIsActive() != null ? req.getIsActive() : "Y");
|
||||
configMapper.insert(vo);
|
||||
return vo.getConfigId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateConfig(Long configId, StatuteConfigSaveReq req) {
|
||||
StatuteConfigResp resp = configMapper.selectById(configId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
StatuteConfigVO vo = new StatuteConfigVO();
|
||||
vo.setConfigId(configId);
|
||||
vo.setEntityType(req.getEntityType());
|
||||
vo.setExpirationMonths(req.getExpirationMonths());
|
||||
vo.setIsActive(req.getIsActive() != null ? req.getIsActive() : resp.getIsActive());
|
||||
configMapper.update(vo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료 대상 채권 일괄 대손 처리.
|
||||
* beforeMonth = baseMonth - expiration_months (YYYYMM 연산).
|
||||
* 처리 건수 반환.
|
||||
*/
|
||||
@Transactional
|
||||
public int expireOverdue(String baseMonth) {
|
||||
StatuteConfigResp config = configMapper.selectByEntityType("CLAWBACK_RECEIVABLE");
|
||||
if (config == null || !"Y".equals(config.getIsActive())) {
|
||||
throw new BizException(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
String beforeMonth = DateUtil.addMonth(baseMonth, -config.getExpirationMonths());
|
||||
return receivableMapper.writeOffExpired(beforeMonth, "EXPIRED");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user