- i18n 사전 100% (break/overtime/leave/clockin) — 50+ 신규 키 - 런타임 재번역 인프라 (ui/i18n_runtime.py) — 재시작 없이 메인 UI 적용 - MealController 분리 — 점심/저녁 토글을 컨트롤러로 추출 - 통합 테스트 +15 (S36-S52: 온보딩/salary/CSV/notification dedupe 등) - pytest 신규 4종 + i18n_runtime 테스트 (총 122 케이스, 90→122) - README/INSTALL/CLAUDE/AGENTS v2.6+ 아키텍처 반영
99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
"""
|
||
core.salary 단위 테스트 — 포괄임금제 외 시급 추정.
|
||
"""
|
||
import os
|
||
import sys
|
||
|
||
import pytest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
from core.salary import estimate_pay, format_won
|
||
|
||
|
||
class TestEstimatePay:
|
||
def test_zero_wage_returns_zero(self):
|
||
out = estimate_pay([{'total_hours': 8, 'overtime_minutes': 30}], 0)
|
||
assert out['base'] == 0
|
||
assert out['overtime'] == 0
|
||
assert out['total'] == 0
|
||
|
||
def test_negative_wage_returns_zero(self):
|
||
out = estimate_pay([{'total_hours': 8, 'overtime_minutes': 0}], -1000)
|
||
assert out['total'] == 0
|
||
|
||
def test_empty_records(self):
|
||
out = estimate_pay([], 10000)
|
||
assert out['base'] == 0
|
||
assert out['overtime'] == 0
|
||
assert out['total'] == 0
|
||
|
||
def test_basic_8h_no_overtime(self):
|
||
# 8h 정규 × 10000 = 80000
|
||
out = estimate_pay([{'total_hours': 8.0, 'overtime_minutes': 0}], 10000)
|
||
assert out['base'] == 80000
|
||
assert out['overtime'] == 0
|
||
assert out['total'] == 80000
|
||
|
||
def test_8h_with_30min_overtime(self):
|
||
# 정규 = 7.5h × 10000 = 75000
|
||
# 연장 = 0.5h × 10000 × 1.5 = 7500
|
||
out = estimate_pay(
|
||
[{'total_hours': 8.0, 'overtime_minutes': 30}],
|
||
hourly_wage=10000,
|
||
overtime_rate=1.5,
|
||
)
|
||
assert out['base'] == pytest.approx(75000)
|
||
assert out['overtime'] == pytest.approx(7500)
|
||
assert out['total'] == pytest.approx(82500)
|
||
|
||
def test_custom_overtime_rate(self):
|
||
# 연장 = 1h × 10000 × 2.0 = 20000
|
||
out = estimate_pay(
|
||
[{'total_hours': 9.0, 'overtime_minutes': 60}],
|
||
hourly_wage=10000,
|
||
overtime_rate=2.0,
|
||
)
|
||
assert out['overtime'] == pytest.approx(20000)
|
||
assert out['base'] == pytest.approx(80000)
|
||
|
||
def test_aggregated_multiple_records(self):
|
||
records = [
|
||
{'total_hours': 8.0, 'overtime_minutes': 0},
|
||
{'total_hours': 9.0, 'overtime_minutes': 60},
|
||
{'total_hours': 8.5, 'overtime_minutes': 30},
|
||
]
|
||
out = estimate_pay(records, hourly_wage=10000)
|
||
# base_hours = 8 + 8 + 8 = 24h
|
||
# overtime_hours = 0 + 1 + 0.5 = 1.5h
|
||
assert out['base_hours'] == pytest.approx(24.0)
|
||
assert out['overtime_hours'] == pytest.approx(1.5)
|
||
assert out['base'] == pytest.approx(240000)
|
||
assert out['overtime'] == pytest.approx(22500) # 1.5 * 10000 * 1.5
|
||
|
||
def test_missing_keys_default_zero(self):
|
||
out = estimate_pay([{}], 10000)
|
||
assert out['total'] == 0
|
||
|
||
def test_overtime_minutes_zero_when_negative_total(self):
|
||
# total - overtime이 음수가 되면 base는 0으로 클램프
|
||
out = estimate_pay(
|
||
[{'total_hours': 0.3, 'overtime_minutes': 60}], # 0.3h - 1h = -0.7
|
||
hourly_wage=10000,
|
||
)
|
||
assert out['base'] == 0
|
||
assert out['overtime'] == pytest.approx(15000)
|
||
|
||
|
||
class TestFormatWon:
|
||
@pytest.mark.parametrize("amount,expected", [
|
||
(0, '0원'),
|
||
(1000, '1,000원'),
|
||
(1234567, '1,234,567원'),
|
||
(999, '999원'),
|
||
(82500.4, '82,500원'), # round
|
||
(82500.6, '82,501원'),
|
||
])
|
||
def test_format(self, amount, expected):
|
||
assert format_won(amount) == expected
|