# 인터페이스 규격서 — income_tax_module (PID 157294)

납품물 DEL-002. 발주처 기존 개발자가 이 문서만으로 모듈을 호출·결합할 수 있도록 작성한다.

## 1. 모듈 개요

| 항목 | 값 |
|------|-----|
| 패키지 | `income_tax_module` |
| 언어 | Python ≥ 3.11 |
| 진입점 | `process_return(inp: ProcessInput) -> ProcessResult` |
| 상태 | **stateless** — 호출 간 저장 없음 |
| 결정성 | 동일 입력 → 동일 출력 (해시 일치 테스트로 증명) |
| 민감정보 | 모듈이 디스크/DB에 저장하지 않음 |

```python
from income_tax_module import process_return
from income_tax_module.models import ProcessInput, WageIncome

result = process_return(
    ProcessInput(
        tax_year=2024,
        case_id="CASE-001",
        wage=WageIncome(gross_pay=48_000_000, income_tax_withheld=2_100_000),
    )
)
print(result.route, result.param_version, result.to_dict())
```

## 2. 입력 계약

### 2.1 `ProcessInput`

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `tax_year` | int | Y | 귀속연도 (파라미터 파일 키) |
| `case_id` | str | Y | 호출 측 식별자 (로그 상관용, 모듈 비저장) |
| `wage` | WageIncome \| null | N | 근로소득 |
| `freelance` | FreelanceIncome \| null | N | 사업자등록 없는 인적용역 |
| `flags` | Flags | N | 수기 판정 플래그 |
| `param_dir` | str \| null | N | 파라미터 디렉터리 오버라이드 |
| `taxpayer` | TaxpayerIdentity \| null | N | 공식 후보 파일 생성용 신고인 정보. 결과 메타에 에코하지 않음 |
| `filing` | FilingSettings | N | 신고유형·납부기한·신고인 설정 |
| `unsupported_deductions` | tuple[str, ...] | N | 미지원 공제 항목. 입력되면 수기 분기 |

### 2.2 `WageIncome`

| 필드 | 타입 | 설명 |
|------|------|------|
| `gross_pay` | int | 총급여 (원) |
| `payer_name` | str | 홈택스 근로소득 지급자 상호 |
| `payer_registration_number` | str | 홈택스 지급자 사업자·주민등록번호(10/13자리) |
| `non_taxable` | int | 비과세 |
| `national_pension` | int | 국민연금 |
| `health_insurance` | int | 건강보험 |
| `long_term_care_insurance` | int | 장기요양보험 |
| `employment_insurance` | int | 고용보험 |
| `industrial_accident_insurance` | int | 산재보험 입력액 |
| `social_insurance_total` | int \| null | 있으면 공제 가능한 개별 사회보험 합계를 대체 |
| `income_tax_withheld` | int | 근로소득세 원천징수 |
| `local_tax_withheld` | int | 근로 지방소득세 특별징수 |

산재보험 입력액은 보존하지만 개인 소득공제 합계에는 포함하지 않는다. 기존 7개 위치 인자
(`gross_pay`부터 `local_tax_withheld`까지)의 순서는 0.1.x와 동일하며 신규 필드는 그 뒤에 있다.

### 2.3 `FreelanceIncome`

| 필드 | 타입 | 설명 |
|------|------|------|
| `revenue` | int | 수입금액 |
| `withheld_tax` | int | 3.3% 등 원천징수세액 |
| `actual_expense` | int \| null | 실제경비. `expense_rate`보다 우선 |
| `expense_rate` | float \| null | 해당 건의 확인된 업종별 경비율(0..1). 두 경비 입력이 모두 없으면 수기 |
| `regional_national_pension` | int | 지역가입 국민연금 |
| `regional_health_insurance` | int | 지역가입 건강보험 |
| `regional_long_term_care_insurance` | int | 지역가입 장기요양보험 |

### 2.4 `Flags`

| 필드 | 기본 | 수기 코드 |
|------|------|-----------|
| `has_business_registration` | false | BUSINESS_REG |
| `financial_income_comprehensive` | false | FINANCIAL_COMPREHENSIVE |
| `has_real_estate_rental` | false | REAL_ESTATE_RENTAL |
| `is_non_resident` | false | NON_RESIDENT |
| `other_manual_reason` | null | OTHER |
| `year_end_settlement_completed` | false | (신고의무 판정 입력) |
| `multiple_employers` | null | (복수 근무지 여부. `null`은 미확인) |
| `all_income_sources_reviewed` | false | (금융·연금·기타소득 등 전체 소득원 확인 완료 여부) |

`year_end_settlement_completed=false`와 `all_income_sources_reviewed=false`, `multiple_employers=null`은 **미확인**이다.
`filing_duty=not_required`는 연말정산 완료·`multiple_employers=false`·전체 소득원 확인 완료·인적용역 없음·수기 플래그 없음·`unsupported_deductions` 없음이 모두 확인될 때만 나오며,
이때 `NO_FILING_DUTY`로 수기 분기한다(환급 목적 자진신고는 별도 선택).

추가 수기: 인적용역 수입이 파라미터 `manual_thresholds.personal_service_revenue` 초과 → `PERSONAL_SERVICE_OVER`.
실제경비와 건별 경비율이 모두 없음 → `EXPENSE_RATE_REQUIRED`; 지원하지 않는 공제 입력 → `UNSUPPORTED_DEDUCTION`.

### 2.5 `TaxpayerIdentity` / `FilingSettings`

`TaxpayerIdentity`는 `name`, 13자리 `resident_registration_number`, `address`, 2자리 `sido_code`,
3자리 `sigungu_code`, 5/10자리 `legal_dong_code`, 선택 `phone`을 받는다.

`FilingSettings` 주요 값은 다음과 같다.

- `return_type`: `regular|amended|late|correction`
- `due_date`: 실제 달력상 유효한 `YYYYMMDD`
- `taxpayer_type`: `01|02|11|19`
- `bookkeeping_duty`: `01|02|03`
- `filing_method`: `11|12|14|20|31|32|35|40`
- `tax_agent_role=00`: 본인신고. 그 외에는 대리인 이름·등록번호·사업자번호·전화번호가 모두 필요

공식 후보 생성 입력에 근로소득이 있으면 미구현 세액공제 확인을 위해 `TAX_CREDIT_SCOPE_REQUIRED`,
환급 예상이면 공식 부호 규칙 확인을 위해 `REFUND_FILING_REVIEW`로 수기 분기한다.

## 3. 출력 계약

### 3.1 `ProcessResult`

| 필드 | 타입 | 설명 |
|------|------|------|
| `ok` | bool | 처리 성공(수기 정상 반환 포함). 파일 검증 실패·입력 오류 시 false |
| `case_id` | str | 입력 에코 |
| `tax_year` | int | 입력 에코 |
| `route` | `"auto"` \| `"manual"` \| `"error"` | 판정. **에러 시 `error` — 수기 큐로 취급 금지** |
| `manual_reasons` | list[{code, message}] | 수기 사유 (auto/error면 []) |
| `param_version` | str | **적용 파라미터 버전** |
| `breakdown` | TaxBreakdown \| null | 수기·에러 시 null |
| `basis` | list[BasisStep] | 단계별 중간값 (버전·세액공제 고지 포함) |
| `filings` | list[FilingArtifact] | 홈택스·위택스 파일 (수기 시 []) |
| `error` | dict \| null | 실패 시 `{code, message, details}` |
| `filing_duty` | `"required"` \| `"not_required"` \| `"unknown"` | **신고의무 축**. 자료 미확인은 `unknown` — 어느 쪽으로도 합치지 않는다 |
| `tax_method` | `"comprehensive"` \| `"unknown"` \| `"none"` | **과세방법 축**. 금융·임대·비거주자 플래그가 있으면 `unknown` |
| `product_path` | `"auto"` \| `"manual"` \| `"error"` | **제품경로 축**. `route`는 이 값의 하위 호환 별칭 |

세 축은 서로 다른 판단이며 한 열로 합치지 않는다 (`docs/INCOME_TAX_CASE_MATRIX.md` §5).
`filing_duty=required`여도 `product_path=manual`일 수 있고, `not_required`여도 자진신고는 가능하다.
`basis`에 `filing_duty`·`tax_method` 단계가 포함된다.

### 3.2 `TaxBreakdown`

`wage_income_amount`, `freelance_income_amount`, `total_taxable`, `income_tax`, `local_income_tax`, `total_tax`,  
`withheld_income_tax`, `withheld_local_tax`, `withheld_total`,  
`payable_income_tax`, `payable_local_tax`, `payable`  
(호환: `wage_taxable`/`freelance_taxable` = 소득금액 별칭)

프리랜서 3.3% 원천은 파라미터 `withholding_33_national_share`(기본 30/33)로 소득세/지방세 분해.

### 3.3 `FilingArtifact`

`kind` (`hometax`|`wetax`), `filename`, `content`, `content_type`, `validation`.

- 신고인 정보가 없으면 기존 JSON 중간 포맷을 반환한다.
- 신고인 정보가 있으면 CP949·CRLF 고정길이 `.101`/`.C10~C40` 후보를 반환한다.
- `content`는 Unicode 전송 문자열이다. 파일 저장 시 반드시 `content.encode("cp949")`로 바이트화한다.
- `validation.local_validation`은 길이·순서·인코딩 사전검사, `authority_validation.status`는 기관 검증 상태다. 로컬 PASS는 기관 접수 성공을 뜻하지 않는다.

## 4. 에러 규약

| code | 의미 |
|------|------|
| `INVALID_INPUT` | 필수값·음수 등 입력 위반 |
| `UNSUPPORTED_TAX_YEAR` | 해당 연도 파라미터 파일 없음 |
| `PARAM_LOAD_FAILED` | 파라미터 파일 손상·불일치 |
| `FILE_VALIDATION_FAILED` | 생성 파일 사전 검증 실패 (`ok=false`, filings에 상세) |
| `INTERNAL` | 예상 밖 예외 |

에러는 예외 전파 대신 **`ProcessResult.ok=false` + `error` 필드**로 반환한다 (`ModuleError`는 내부·선택적 raise).

## 5. 파라미터

- 경로: `income_tax_module/params/data/{tax_year}.json`
- 파일 교체만으로 반영 (`load_params`). 해당 연도 파일이 없으면 `UNSUPPORTED_TAX_YEAR`로 거부한다
- 파일럿 버전: `params-2024.1.0-pilot`, `params-2025.1.0-pilot`
- 각 파일은 `confirmed`(근거 확인된 키)와 `unconfirmed`(키 → 미확정 사유)를 명시한다
- `unconfirmed` 키를 읽으면 `PARAM_LOAD_FAILED`로 거부한다. 현재 `personal_service_simple_expense_rate`가 여기 해당하므로, 인적용역 경비는 `freelance.expense_rate` 또는 `actual_expense`로 건별 확정값을 넣어야 한다
- `manual_thresholds`는 세법 기준이 아니라 수기 전환용 보수적 가드값이다

## 6. 통합 시나리오 3종

### 6.1 단일소득 (근로) — `fixtures/wage_only.json`

1. 입력: 근로만, flags 전부 false  
2. 기대: `route=auto`, breakdown·basis·filings 2건, validation pass  

### 6.2 겸업 — `fixtures/combined.json`

1. 입력: wage + freelance  
2. 기대: `route=auto`, wage_taxable·freelance_taxable 모두 > 0  

### 6.3 수기분기 — `fixtures/manual_business.json`

1. 입력: `has_business_registration=true`  
2. 기대: `route=manual`, reason `BUSINESS_REG`, breakdown=null, filings=[]  

## 7. 호출 예제 (CLI)

```bash
cd /path/to/wishket-pilot-157294-income-tax-module
PYTHONPATH=src python3 -c "
import json
from pathlib import Path
from income_tax_module import process_return
from income_tax_module.models import ProcessInput
raw = json.loads(Path('fixtures/combined.json').read_text())
print(json.dumps(process_return(ProcessInput.from_dict(raw)).to_dict(), ensure_ascii=False, indent=2)[:800])
"
```

## 8. 시연 하네스 HTTP

| Method | Path | 설명 |
|--------|------|------|
| GET | `/` | 공개 제안 |
| GET | `/demo` | 시나리오 선택 |
| GET | `/demo/run?scenario=wage\|combined\|manual` | 호출 결과 |
| GET | `/demo/params` | 파라미터 버전 |
| GET | `/api/process?scenario=` | JSON 결과 |
| GET | `/docs/interface` | 본 규격서 |
| GET | `/health` | 헬스 |

```bash
PYTHONPATH=src uvicorn harness.app:app --host 127.0.0.1 --port 8029
```

## 9. 경계

- 스크래핑·전자신고 **제출** API 연동 없음  
- 신고 건 상태 관리·재처리·알림 없음  
- 파일 포맷은 **파일럿 중간 스키마** (`docs/FILING_SCHEMA.md`) — 공식 홈택스/위택스 바이너리 레이아웃 확정 전  
- 실제 세액 최종 확인은 세무 전문가 검토 전제  
