Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ntropy.common.client;

import com.ntropy.common.dto.account.VirtualSettlementDepositCommand;
import com.ntropy.common.dto.account.VirtualSettlementDepositResult;

/** NTROPY 가상계좌에 플랫폼 정산 입금 거래를 멱등하게 생성하는 내부 명령 계약. */
public interface VirtualSettlementDepositCommandClient {

/** 누적 목표액에서 기존 생성액을 뺀 차액 거래를 만들고, 해당 정산기간의 매칭 가능 여부를 반환한다. */
VirtualSettlementDepositResult createOrAdjust(VirtualSettlementDepositCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ntropy.common.dto.account;

import java.time.LocalDate;

/**
* work-service가 계산한 플랫폼 정산금의 누적 목표액을 NTROPY 가상 수시입출금 계좌에 기록하는 명령.
* 사용자·플랫폼·정산기간은 account-service가 멱등 fingerprint를 만드는 논리 키로 사용한다.
*/
public record VirtualSettlementDepositCommand(
Long userId,
Long platformId,
LocalDate periodStart,
LocalDate periodEnd,
LocalDate depositDate,
Long amount,
String depositName
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ntropy.common.dto.account;

/** 가상 정산 입금 명령 결과. available이면 해당 정산기간을 실제 입금 조회로 매칭할 수 있다. */
public record VirtualSettlementDepositResult(
boolean available,
boolean transactionCreated
) {

public static VirtualSettlementDepositResult unavailable() {
return new VirtualSettlementDepositResult(false, false);
}

public static VirtualSettlementDepositResult alreadyAvailable() {
return new VirtualSettlementDepositResult(true, false);
}

public static VirtualSettlementDepositResult created() {
return new VirtualSettlementDepositResult(true, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.ntropy.account.client;

import java.math.BigDecimal;
import java.time.LocalTime;
import java.util.List;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.ntropy.account.domain.AccountGroup;
import com.ntropy.account.domain.AccountTransactionCategory;
import com.ntropy.account.domain.ConnectionProvider;
import com.ntropy.account.domain.TransactionFingerprint;
import com.ntropy.account.domain.entity.Account;
import com.ntropy.account.domain.entity.AccountTransaction;
import com.ntropy.account.mapper.AccountMapper;
import com.ntropy.account.mapper.VirtualSettlementDepositMapper;
import com.ntropy.common.client.VirtualSettlementDepositCommandClient;
import com.ntropy.common.dto.account.VirtualSettlementDepositCommand;
import com.ntropy.common.dto.account.VirtualSettlementDepositResult;

import lombok.RequiredArgsConstructor;

/** 플랫폼 예상 정산금을 사용자의 NTROPY 가상 수시입출금 계좌에 기록한다. */
@Component
@RequiredArgsConstructor
public class LocalVirtualSettlementDepositCommandClient implements VirtualSettlementDepositCommandClient {

private static final String ORDINARY_DEPOSIT_TYPE_CODE = "11";
private static final String FINGERPRINT_TYPE = "VIRTUAL_PLATFORM_SETTLEMENT";

private final AccountMapper accountMapper;
private final VirtualSettlementDepositMapper depositMapper;

@Override
@Transactional
public VirtualSettlementDepositResult createOrAdjust(VirtualSettlementDepositCommand command) {
validate(command);
Account account = findVirtualOrdinaryAccount(command.userId());
if (account == null) {
return VirtualSettlementDepositResult.unavailable();
}

String settlementKey = "VS:" + TransactionFingerprint.hash(
command.userId(),
command.platformId(),
command.periodStart(),
command.periodEnd(),
command.depositDate()
);
BigDecimal lockedBalance = depositMapper.findBalanceForUpdate(account.getId());
BigDecimal currentBalance = lockedBalance == null ? BigDecimal.ZERO : lockedBalance;
BigDecimal generatedAmount = depositMapper.sumGeneratedAmount(account.getId(), settlementKey);
if (generatedAmount == null) {
generatedAmount = BigDecimal.ZERO;
}
BigDecimal amount = BigDecimal.valueOf(command.amount()).subtract(generatedAmount);
if (amount.signum() <= 0) {
return VirtualSettlementDepositResult.alreadyAvailable();
}

AccountTransaction transaction = new AccountTransaction();
transaction.setAccountId(account.getId());
transaction.setFingerprint(TransactionFingerprint.hash(
FINGERPRINT_TYPE,
settlementKey,
command.amount()
));
transaction.setTransactionCategory(AccountTransactionCategory.ORDINARY);
transaction.setTranDate(command.depositDate());
transaction.setTranTime(LocalTime.of(6, Math.floorMod(command.platformId().intValue(), 60)));
transaction.setOutAmount(BigDecimal.ZERO);
transaction.setInAmount(amount);
transaction.setAfterBalance(currentBalance.add(amount));
// IncomingCounterpartyNameExtractor가 IBK는 desc1, 나머지 은행은 desc3을 사용한다.
transaction.setDesc1(command.depositName());
transaction.setDesc2("정산입금");
transaction.setDesc3(command.depositName());
transaction.setDesc4(settlementKey);

int inserted = depositMapper.insertIfAbsent(transaction);
if (inserted != 1) {
return VirtualSettlementDepositResult.alreadyAvailable();
}
if (depositMapper.incrementBalanceAndAdvanceLastTranDate(
account.getId(), amount, command.depositDate()) != 1) {
throw new IllegalStateException("가상 정산 입금 계좌 잔액 갱신에 실패했습니다: accountId=" + account.getId());
}
return VirtualSettlementDepositResult.created();
}

private Account findVirtualOrdinaryAccount(Long userId) {
List<Account> accounts = accountMapper.findByUserIdAndProvider(userId, ConnectionProvider.NTROPY.name());
if (accounts == null) {
return null;
}
return accounts.stream()
.filter(account -> account.getAccountGroup() == AccountGroup.DEPOSIT_TRUST)
.filter(account -> ORDINARY_DEPOSIT_TYPE_CODE.equals(account.getDepositTypeCode()))
.findFirst()
.orElse(null);
}

private static void validate(VirtualSettlementDepositCommand command) {
if (command == null
|| command.userId() == null
|| command.platformId() == null
|| command.periodStart() == null
|| command.periodEnd() == null
|| command.depositDate() == null
|| command.amount() == null
|| command.amount() <= 0
|| command.depositName() == null
|| command.depositName().isBlank()) {
throw new IllegalArgumentException("유효한 가상 정산 입금 명령이 필요합니다");
}
if (command.periodStart().isAfter(command.periodEnd())) {
throw new IllegalArgumentException("정산기간 시작일은 종료일 이후일 수 없습니다");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ntropy.account.mapper;

import java.math.BigDecimal;
import java.time.LocalDate;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.ntropy.account.domain.entity.AccountTransaction;

/** NTROPY 가상 정산 거래의 멱등 저장과 계좌 잔액 반영 전용 Mapper. */
@Mapper
public interface VirtualSettlementDepositMapper {

BigDecimal findBalanceForUpdate(@Param("accountId") Long accountId);

BigDecimal sumGeneratedAmount(
@Param("accountId") Long accountId,
@Param("settlementKey") String settlementKey);

int insertIfAbsent(AccountTransaction transaction);

int incrementBalanceAndAdvanceLastTranDate(
@Param("accountId") Long accountId,
@Param("amount") BigDecimal amount,
@Param("tranDate") LocalDate tranDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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.ntropy.account.mapper.VirtualSettlementDepositMapper">

<select id="findBalanceForUpdate" resultType="java.math.BigDecimal">
SELECT balance
FROM ACCOUNT
WHERE account_id = #{accountId}
FOR UPDATE
</select>

<select id="sumGeneratedAmount" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(in_amount), 0)
FROM ACCOUNT_TRANSACTION
WHERE account_id = #{accountId}
AND transaction_category = 'ORDINARY'
AND desc4 = #{settlementKey}
</select>

<insert id="insertIfAbsent" parameterType="com.ntropy.account.domain.entity.AccountTransaction">
INSERT IGNORE INTO ACCOUNT_TRANSACTION (
account_id, fingerprint, transaction_category,
tran_date, tran_time, out_amount, in_amount, after_balance,
desc1, desc2, desc3, desc4
) VALUES (
#{accountId}, #{fingerprint}, #{transactionCategory},
#{tranDate}, #{tranTime}, #{outAmount}, #{inAmount}, #{afterBalance},
#{desc1}, #{desc2}, #{desc3}, #{desc4}
)
</insert>

<update id="incrementBalanceAndAdvanceLastTranDate">
UPDATE ACCOUNT
SET balance = COALESCE(balance, 0) + #{amount},
last_tran_date = CASE
WHEN last_tran_date IS NULL OR last_tran_date &lt; #{tranDate} THEN #{tranDate}
ELSE last_tran_date
END
WHERE account_id = #{accountId}
</update>

</mapper>
Loading
Loading