# Webhook 분류 로직 수정 + Amount 변환 수정 지침서

> **목적**: (1) INTERNAL_TRANSFER 분류 제거 → 내부 이체도 to 지갑 타입 기반 정상 분류, (2) wei→토큰 단위 변환
> **날짜**: 2026-03-21
> **대상 모듈**: open-api
> **발견 계기**: Phase 3 E2E 테스트 — ADMIN→HOT USDT 전송 Webhook이 INTERNAL_TRANSFER로 분류되어 입금 미처리, amount DECIMAL 오버플로

---

## 1. 문제 요약

### 1-1. INTERNAL_TRANSFER 분류 문제

| 상황 | 현재 동작 | 기대 동작 |
|------|-----------|-----------|
| ADMIN → HOT 자금 이동 | `INTERNAL_TRANSFER` (무시) | `DEPOSIT` (입금 처리) |
| GAS → HOT 가스 전송 | `INTERNAL_TRANSFER` (무시) | `FUNDING` (로그만) |
| HOT → MASTER 집금 (미큐잉) | `INTERNAL_TRANSFER` (무시) | `DEPOSIT` 또는 수동 확인 |

**원인**: `BusinessEventClassifier.classify()`에서 `toIsOurs && fromIsOurs` → `collectionQueue` 없으면 무조건 `INTERNAL_TRANSFER` 반환.

### 1-2. Amount DECIMAL 오버플로 문제

| 항목 | 값 |
|------|-----|
| Webhook amount | `"1000000000000000000"` (1 USDT in wei, 10^18) |
| DB 컬럼 | `DECIMAL(36,18)` — 정수부 최대 18자리 |
| 10^18 | **19자리** → 오버플로 |

**원인**: `WebhookProcessingService.processDeposit()`에서 `new BigDecimal(dto.getAmount())`로 raw wei 값을 변환 없이 저장.

---

## 2. 수정 사항

### 2-1. BusinessEventClassifier.java — INTERNAL_TRANSFER 제거

**파일**: `open-api/src/main/java/com/cryptoments/webhook/service/BusinessEventClassifier.java`

**변경 위치**: `classify()` 메서드 내 `toIsOurs && fromIsOurs` 블록 (기존 약 line 72~78)

**Before**:
```java
// 2. 양쪽 다 우리 주소 — 내부 이체
if (toIsOurs && fromIsOurs) {
    if (collectionQueueRepository.findByTxHash(dto.getTxHash()) != null) {
        return "COLLECTION_CONFIRM";
    }
    return "INTERNAL_TRANSFER";
}
```

**After**:
```java
// 2. 양쪽 다 우리 주소
if (toIsOurs && fromIsOurs) {
    if (collectionQueueRepository.findByTxHash(dto.getTxHash()) != null) {
        return "COLLECTION_CONFIRM";
    }
    if (!withdrawalRepository.findByTxHash(dto.getTxHash()).isEmpty()) {
        return "WITHDRAWAL_CONFIRM";
    }
    // 내부 자금 이동이라도 to가 HOT/POOL/MASTER면 입금으로 처리
    String toWalletType = toWallet.getWalletType().name();
    if ("HOT".equals(toWalletType) || "POOL".equals(toWalletType) || "MASTER".equals(toWalletType)) {
        return "DEPOSIT";
    }
    // GAS/ADMIN 등 인프라 지갑 간 이체 — 로그만
    log.info("인프라 자금 이동: txHash={}, from={}, to={}", dto.getTxHash(), dto.getFrom(), dto.getTo());
    return "FUNDING";
}
```

**분류 결과 정리**:

| from | to | collection_queue | withdrawal | 결과 |
|------|----|-----------------|------------|------|
| 우리 | 우리(HOT/POOL/MASTER) | 있음 | - | COLLECTION_CONFIRM |
| 우리 | 우리(HOT/POOL/MASTER) | 없음 | 있음 | WITHDRAWAL_CONFIRM |
| 우리 | 우리(HOT/POOL/MASTER) | 없음 | 없음 | **DEPOSIT** |
| 우리 | 우리(GAS/ADMIN) | 없음 | 없음 | **FUNDING** |
| 외부 | 우리(HOT/POOL) | - | - | DEPOSIT (기존 유지) |
| 우리 | 외부 | - | 있음 | WITHDRAWAL_CONFIRM (기존 유지) |

---

### 2-2. WebhookProcessingService.java — switch 문 수정

**파일**: `open-api/src/main/java/com/cryptoments/webhook/service/WebhookProcessingService.java`

**변경 위치**: `process()` 메서드 내 switch 문

**Before**:
```java
switch (businessType) {
    case "DEPOSIT" -> processDeposit(dto, network);
    case "WITHDRAWAL_CONFIRM" -> confirmWithdrawal(dto);
    case "COLLECTION_CONFIRM" -> confirmCollection(dto);
    case "TX_FAILED" -> handleFailedTx(dto);
    case "INTERNAL_TRANSFER" -> log.info("내부 이체 감지: txHash={}", dto.getTxHash());
    default -> log.warn("미처리 비즈니스 타입: {} (txHash={})", businessType, dto.getTxHash());
}
```

**After**:
```java
switch (businessType) {
    case "DEPOSIT" -> processDeposit(dto, network);
    case "WITHDRAWAL_CONFIRM" -> confirmWithdrawal(dto);
    case "COLLECTION_CONFIRM" -> confirmCollection(dto);
    case "TX_FAILED" -> handleFailedTx(dto);
    case "FUNDING" -> log.info("인프라 자금 이동 (처리 불필요): txHash={}", dto.getTxHash());
    default -> log.warn("미처리 비즈니스 타입: {} (txHash={})", businessType, dto.getTxHash());
}
```

> `INTERNAL_TRANSFER` 케이스 완전 제거, `FUNDING` 케이스 추가.

---

### 2-3. WebhookProcessingService.java — Amount wei→토큰 변환

**파일**: `open-api/src/main/java/com/cryptoments/webhook/service/WebhookProcessingService.java`

**변경 위치**: `processDeposit()` 메서드 내 Deposit.builder() 직전

**Before**:
```java
Deposit deposit = Deposit.builder()
        .depositCode(generateDepositCode())
        // ...
        .amount(new BigDecimal(dto.getAmount()))
        // ...
        .build();
```

**After**:
```java
// wei → 토큰 단위 변환
BigDecimal rawAmount = new BigDecimal(dto.getAmount());
int decimals = (dto.getTokenDecimals() != null) ? dto.getTokenDecimals() : 18;
BigDecimal convertedAmount = rawAmount.movePointLeft(decimals);

Deposit deposit = Deposit.builder()
        .depositCode(generateDepositCode())
        // ...
        .amount(convertedAmount)
        // ...
        .build();
```

**변환 예시**:

| Webhook amount | tokenDecimals | 변환 결과 | DB 저장값 |
|---------------|--------------|-----------|----------|
| `"1000000000000000000"` | 18 | `1.000000000000000000` | 1.0 USDT |
| `"500000000000000000"` | 18 | `0.500000000000000000` | 0.5 USDT |
| `"1000000"` | 6 | `1.000000` | 1.0 USDC(6 decimals) |

> **주의**: `assetType == "NATIVE"` (BNB, MATIC 등)인 경우에도 blockchain monitor가 wei 단위로 보내므로 동일 변환 적용. 네이티브 코인의 decimals도 18이므로 문제 없음.

---

## 3. 적용 체크리스트

| # | 작업 | 파일 |
|---|------|------|
| 1 | BusinessEventClassifier — INTERNAL_TRANSFER → DEPOSIT/FUNDING 분기 (섹션 2-1) | `BusinessEventClassifier.java` |
| 2 | WebhookProcessingService — switch 문 FUNDING 추가, INTERNAL_TRANSFER 제거 (섹션 2-2) | `WebhookProcessingService.java` |
| 3 | WebhookProcessingService — amount wei→토큰 변환 (섹션 2-3) | `WebhookProcessingService.java` |
| 4 | `./gradlew :open-api:compileJava` — 컴파일 확인 | - |
| 5 | open-api 재기동 | - |

---

## 4. 검증 시나리오

### 4-1. 외부→HOT 입금 (기존 동작 유지 확인)

```bash
curl -X POST http://localhost:8082/api/v2/webhooks/blockchain-monitor \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "TRANSFER",
    "chainId": "56",
    "status": "CONFIRMED",
    "txHash": "0xtest_external_deposit",
    "from": "0x_외부_주소",
    "to": "0x66059E8C81D5E400D546B07a356922d1F0D158E5",
    "amount": "1000000000000000000",
    "tokenSymbol": "USDT",
    "contractAddress": "0x55d398326f99059fF775485246999027B3197955",
    "tokenDecimals": 18
  }'
```
**기대**: `businessType=DEPOSIT`, deposits 테이블에 `amount=1.0` INSERT

### 4-2. ADMIN→HOT 내부 이체 → DEPOSIT 분류

```bash
curl -X POST http://localhost:8082/api/v2/webhooks/blockchain-monitor \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "TRANSFER",
    "chainId": "56",
    "status": "CONFIRMED",
    "txHash": "0xtest_admin_to_hot",
    "from": "0xF9cbB86D5fae82183AA8e0E4fFf05a4C917414FE",
    "to": "0x66059E8C81D5E400D546B07a356922d1F0D158E5",
    "amount": "5000000000000000000",
    "tokenSymbol": "USDT",
    "contractAddress": "0x55d398326f99059fF775485246999027B3197955",
    "tokenDecimals": 18
  }'
```
**기대**: `businessType=DEPOSIT` (NOT INTERNAL_TRANSFER), deposits에 `amount=5.0` INSERT

### 4-3. 멱등성 — 동일 txHash 재전송

동일 txHash로 다시 전송 → deposits 행 수 변화 없음 (로그: "입금 중복 감지")

### 4-4. GAS/ADMIN 간 이체 → FUNDING

from=ADMIN, to=GAS 지갑 주소로 전송 → `businessType=FUNDING`, deposits INSERT 없음

---

## 5. 부가 이슈: Webhook 잔액 동기화 실패

로그에서 `Webhook 잔액 동기화 실패: address=..., error=null` 발생.

**원인 추정**: `WebhookBalanceSyncService.syncAffectedWallets()`에서 tokenSymbol → currency 조회 실패 또는 blockchain-api balance 조회 실패.

**대응**: Phase 3 E2E 입금 파이프라인 정상화 후 별도 확인. 잔액 동기화는 백업 메커니즘이므로 입금 처리에는 영향 없음.
