diff --git a/.gitignore b/.gitignore index 2d27cdd..40f6c98 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,11 @@ htmlcov/ *.sqlite3 mysql_data/ data/ +# 런타임에 필요한 집계 통계 파일만 예외적으로 커밋한다(개인 단위 원본 데이터 아님). +# 디렉터리를 먼저 풀어준 뒤 그 안의 나머지 파일은 다시 전부 무시하고, 대상 파일 하나만 재포함한다. +!fastapi/data/ +fastapi/data/* +!fastapi/data/active_income_stats.csv # ========================= # Docker diff --git a/fastapi/app/error_handlers.py b/fastapi/app/error_handlers.py new file mode 100644 index 0000000..5533ce5 --- /dev/null +++ b/fastapi/app/error_handlers.py @@ -0,0 +1,78 @@ +"""전 엔드포인트의 에러 응답을 app/schemas/errors.py의 단일 포맷으로 통일하는 핸들러. + +- RequestValidationError(Pydantic 422): 필드별 원인을 한국어로 번역해 details에 담는다. +- HTTPException(라우터가 ValueError를 잡아 400/500으로 던진 것): 동일 포맷으로 감싼다. +""" + +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +from app.schemas.errors import ErrorDetail, ErrorResponse + +_PYDANTIC_ERROR_MESSAGES: dict[str, str] = { + "missing": "필수 항목입니다.", + "int_parsing": "정수여야 합니다.", + "int_type": "정수여야 합니다.", + "float_parsing": "숫자여야 합니다.", + "float_type": "숫자여야 합니다.", + "string_type": "문자열이어야 합니다.", + "enum": "허용되지 않는 값입니다.", + "bool_parsing": "true/false 값이어야 합니다.", + "bool_type": "true/false 값이어야 합니다.", +} + + +def _translate_pydantic_error(error: dict) -> str: + """Pydantic 에러 하나를 한국어 문구로 번역한다. 매핑에 없는 타입은 원문 메시지를 그대로 쓴다.""" + error_type = error.get("type", "") + if error_type == "value_error": + # model_validator가 던진 ValueError는 이미 한국어지만, Pydantic이 "Value error, " 접두사를 붙인다. + return error.get("msg", "").removeprefix("Value error, ") + return _PYDANTIC_ERROR_MESSAGES.get(error_type, error.get("msg", "입력값이 올바르지 않습니다.")) + + +def _field_name(loc: tuple) -> str | None: + """Pydantic의 loc(예: ("body", "current_age"))를 "current_age" 같은 점 표기 필드명으로 변환한다. + 특정 필드에 속하지 않는 에러(예: 모델 전체에 대한 검증)면 None을 반환한다.""" + path = [str(part) for part in loc if part != "body"] + return ".".join(path) if path else None + + +def _validation_error_message(details: list[ErrorDetail]) -> str: + if len(details) == 1: + return details[0].reason + return f"입력값을 확인해주세요. ({len(details)}개 항목에 문제가 있습니다.)" + + +_STATUS_CODE_TO_ERROR_CODE: dict[int, str] = { + 400: "INVALID_INPUT", + 404: "NOT_FOUND", + 500: "INTERNAL_ERROR", +} + + +def _error_code_for_status(status_code: int) -> str: + return _STATUS_CODE_TO_ERROR_CODE.get(status_code, "ERROR") + + +def register_error_handlers(app: FastAPI) -> None: + @app.exception_handler(RequestValidationError) + async def _handle_validation_error(request: Request, exc: RequestValidationError) -> JSONResponse: + details = [ + ErrorDetail(field=_field_name(error["loc"]), reason=_translate_pydantic_error(error)) + for error in exc.errors() + ] + body = ErrorResponse( + code="VALIDATION_ERROR", + message=_validation_error_message(details), + details=details, + ) + return JSONResponse(status_code=422, content=body.model_dump()) + + @app.exception_handler(StarletteHTTPException) + async def _handle_http_exception(request: Request, exc: StarletteHTTPException) -> JSONResponse: + message = exc.detail if isinstance(exc.detail, str) else "요청을 처리할 수 없습니다." + body = ErrorResponse(code=_error_code_for_status(exc.status_code), message=message, details=[]) + return JSONResponse(status_code=exc.status_code, content=body.model_dump()) diff --git a/fastapi/app/main.py b/fastapi/app/main.py index b2275c1..252e0f9 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -1,14 +1,28 @@ +from contextlib import asynccontextmanager + from fastapi import FastAPI +from app.error_handlers import register_error_handlers +from app.repositories.employees_income_repository import ensure_data_available from app.routers import employees, retirement + +@asynccontextmanager +async def lifespan(app: FastAPI): + ensure_data_available() + yield + + app = FastAPI( title="ORBIT FastAPI Server", description="ORBIT FastAPI Server", version="0.1.0", root_path="/ai", + lifespan=lifespan, ) +register_error_handlers(app) + app.include_router(employees.router) app.include_router(retirement.router) diff --git a/fastapi/app/repositories/employees_income_repository.py b/fastapi/app/repositories/employees_income_repository.py index a198239..811d0e6 100644 --- a/fastapi/app/repositories/employees_income_repository.py +++ b/fastapi/app/repositories/employees_income_repository.py @@ -6,6 +6,19 @@ _stats: pd.DataFrame | None = None +def ensure_data_available() -> None: + """/api/employees/simulate에 필요한 소득 통계 CSV의 존재를 앱 기동 시점에 검증한다. + + 파일이 없으면 첫 요청에서 500이 나는 대신, 여기서 즉시 RuntimeError로 기동을 실패시킨다. + """ + if not _CSV_PATH.exists(): + raise RuntimeError( + f"필수 데이터 파일이 없습니다: {_CSV_PATH}\n" + "재직자 연금 시뮬레이션(/api/employees/simulate)에 필요한 소득 통계 파일(active_income_stats.csv)입니다. " + "scripts/preprocess.py로 생성하거나, 배포 환경에서 데이터 볼륨이 올바르게 마운트됐는지 확인하세요." + ) + + def _get_stats() -> pd.DataFrame: global _stats if _stats is None: diff --git a/fastapi/app/schemas/errors.py b/fastapi/app/schemas/errors.py new file mode 100644 index 0000000..27c4911 --- /dev/null +++ b/fastapi/app/schemas/errors.py @@ -0,0 +1,20 @@ +"""전 엔드포인트가 공유하는 단일 에러 응답 스키마. + +Pydantic 422(RequestValidationError)와 라우터의 HTTPException(400/500 등)을 +app/main.py의 예외 핸들러가 전부 이 형태로 변환해서 내보낸다. 프론트는 항상 +code로 분기하고, message를 사용자에게 그대로 보여주고, details로 어떤 필드가 +문제인지 알 수 있다. +""" + +from pydantic import BaseModel + + +class ErrorDetail(BaseModel): + field: str | None # 문제가 된 필드명(점 표기, 예: "current_age"). 특정 필드에 속하지 않으면 None + reason: str # 해당 필드가 왜 문제인지에 대한 한국어 설명 + + +class ErrorResponse(BaseModel): + code: str # 프론트 분기용 에러 코드 (예: VALIDATION_ERROR, INVALID_INPUT, INTERNAL_ERROR) + message: str # 사용자에게 그대로 보여줄 수 있는 한국어 메시지 + details: list[ErrorDetail] = [] # 필드별 상세 (없으면 빈 리스트) diff --git a/fastapi/app/schemas/money.py b/fastapi/app/schemas/money.py new file mode 100644 index 0000000..dfbe118 --- /dev/null +++ b/fastapi/app/schemas/money.py @@ -0,0 +1,30 @@ +"""API 경계의 금액 단위(원) <-> 내부 계산 단위(만원) 변환을 한 곳에 모은 모듈. + +app/services/* 의 계산 로직과 상수(INVESTMENT_RETURN, reduction_rules의 threshold 등)는 +전부 만원 단위를 그대로 사용한다(변경하지 않음). 이 모듈이 제공하는 타입 별칭을 +요청/응답 스키마의 금액 필드에 붙이면, Pydantic이 역직렬화/직렬화 시점에 자동으로 +원 <-> 만원 변환을 수행한다. 다른 곳에서 별도로 곱셈/나눗셈을 하지 않는다. +""" + +from typing import Annotated + +from pydantic import BeforeValidator, PlainSerializer + +WON_PER_MANWON = 10_000 + + +def won_to_manwon(value: float) -> float: + """API 요청으로 들어온 원 단위 금액을 내부 계산용 만원 단위로 변환한다.""" + return value / WON_PER_MANWON + + +def manwon_to_won(value: float) -> int: + """내부 계산 결과(만원)를 API 응답용 원 단위 정수로 변환한다.""" + return round(value * WON_PER_MANWON) + + +WonAmountInput = Annotated[float, BeforeValidator(won_to_manwon)] +"""요청 스키마의 금액 필드에 사용한다. API 입력은 원 단위, 필드에는 만원 단위로 저장된다.""" + +WonAmountOutput = Annotated[float, PlainSerializer(manwon_to_won, return_type=int)] +"""응답 스키마의 금액 필드에 사용한다. 내부(만원) 값을 API 출력 시 원 단위 정수로 직렬화한다.""" diff --git a/fastapi/app/schemas/retirement.py b/fastapi/app/schemas/retirement.py index 74dda83..1a86ea8 100644 --- a/fastapi/app/schemas/retirement.py +++ b/fastapi/app/schemas/retirement.py @@ -2,6 +2,15 @@ from pydantic import BaseModel +from app.schemas.money import WonAmountInput, WonAmountOutput + + +class Gender(str, Enum): + """성별. target_age(목표연령) 산정에 사용된다.""" + + MALE = "male" + FEMALE = "female" + class ReadinessStatus(str, Enum): """노후 준비 상태 판정 결과""" @@ -13,17 +22,18 @@ class ReadinessStatus(str, Enum): class TimelinePoint(BaseModel): age: int # 나이 - asset: float # 해당 나이 시점의 자산 - income: float # 해당 연도 소득(연금) - expense: float # 해당 연도 지출 - gap: float # 연간 Gap (지출 - 소득) - cumulative_gap: float # 누적 Gap + asset: WonAmountOutput # 해당 나이 시점의 자산 (원) + annual_income: WonAmountOutput # 해당 연도 소득(연금) (원, 연 단위) + annual_expense: WonAmountOutput # 해당 연도 지출 (원, 연 단위) + annual_gap: WonAmountOutput # 연간 Gap (지출 - 소득) (원, 연 단위) + cumulative_annual_gap: WonAmountOutput # 누적 Gap (원, annual_gap의 누적합) class SimulationResult(BaseModel): current_age: int # 현재 나이 - monthly_gap: float # 현재 시점의 월 Gap (월 생활비 - 월 연금) + monthly_gap: WonAmountOutput # 현재 시점의 월 Gap (월 생활비 - 월 연금) (원) depletion_age: int | None # 자산 고갈 나이 (고갈되지 않으면 None) + depleted: bool # 자산 고갈 여부 (depletion_age is not None과 항상 일치) target_age: int # 목표연령 (성별 고정값) status: ReadinessStatus # 노후 준비 상태 timeline: list[TimelinePoint] # 연도별 자산 추이 @@ -31,10 +41,10 @@ class SimulationResult(BaseModel): class DiagnosisRequest(BaseModel): current_age: int # 현재 나이 - monthly_expenses: float # 월 생활비 - monthly_pension: float # 월 연금 수령액 - asset: float # 현재 보유 자산 - gender: str # 성별 ("male" 또는 "female") + monthly_expenses: WonAmountInput # 월 생활비 (원) + monthly_pension: WonAmountInput # 월 연금 수령액 (원) + asset: WonAmountInput # 현재 보유 자산 (원) + gender: Gender # 성별 class RecommendationRequest(DiagnosisRequest): @@ -52,10 +62,11 @@ class RecommendationType(str, Enum): class RecommendationResult(BaseModel): current_age: int # 현재 나이 recommendation_type: RecommendationType # 추천 유형 - required_saving: float # 필요 월 절약액 (만원) - required_income: float # 필요 월 추가 소득액 (만원) + required_saving: WonAmountOutput # 필요 월 절약액 (원) + required_income: WonAmountOutput # 필요 월 추가 소득액 (원) target_status: ReadinessStatus # 추천 산정에 사용된 목표 기준 (항상 SUFFICIENT="고갈 없음") depletion_age: int | None # 개선 적용 후 자산 고갈 나이 + depleted: bool # 개선 적용 후 자산 고갈 여부 (depletion_age is not None과 항상 일치) target_age: int # 목표연령 status: ReadinessStatus # 개선 적용 후 노후 준비 상태 timeline: list[TimelinePoint] # 개선 적용 후 연도별 자산 추이 @@ -64,17 +75,18 @@ class RecommendationResult(BaseModel): class ReductionRequest(DiagnosisRequest): """진단(diagnosis)과 동일한 입력 스키마를 재사용하고 재취업 관련 필드를 추가""" - reemployment_income: float # 재취업 예상 월소득 (만원) + reemployment_income: WonAmountInput # 재취업 예상 월소득 (원) year: int | None = None # 소득심사 기준 연도 (미지정 시 최신 규칙 적용) class ReductionResult(BaseModel): current_age: int # 현재 나이 - reemployment_income: float # 재취업 예상 월소득 (만원) - monthly_reduction: float # 월 감액액 (만원) - reduced_monthly_pension: float # 감액 후 월 실수령 연금액 (만원) - full_payment_income_threshold: float # 전액 수령 가능 소득 상한 (만원) + reemployment_income: WonAmountOutput # 재취업 예상 월소득 (원) + monthly_reduction: WonAmountOutput # 월 감액액 (원) + reduced_monthly_pension: WonAmountOutput # 감액 후 월 실수령 연금액 (원) + full_payment_income_threshold: WonAmountOutput # 전액 수령 가능 소득 상한 (원) depletion_age: int | None # 감액 반영 후 자산 고갈 나이 + depleted: bool # 감액 반영 후 자산 고갈 여부 (depletion_age is not None과 항상 일치) target_age: int # 목표연령 status: ReadinessStatus # 감액 반영 후 노후 준비 상태 timeline: list[TimelinePoint] # 감액 반영 후 연도별 자산 추이 @@ -84,7 +96,7 @@ class ScenariosRequest(DiagnosisRequest): """진단(diagnosis)과 동일한 입력 스키마를 재사용하고 수령방식 계산에 필요한 필드를 추가""" early_years: int = 5 # 조기수령 연수 (1~5년, 1년당 5% 감액) - base_monthly_income: float # 기준소득월액 (만원) - LUMP_SUM/SPLIT 공제일시금 산식에 사용 + base_monthly_income: WonAmountInput # 기준소득월액 (원) - LUMP_SUM/SPLIT 공제일시금 산식에 사용 total_service_years: int # 총 재직연수 - LUMP_SUM/SPLIT 공제일시금 산식에 사용 deduction_years: int | None = None # SPLIT(분할수령) 공제연수. 미지정 시 제약 내 최댓값으로 클램프 @@ -100,8 +112,9 @@ class ScenarioType(str, Enum): class ScenarioOutcome(BaseModel): scenario_type: ScenarioType # 수령방식 - depletion_age: int # 자산 고갈 나이 (고갈되지 않으면 MAX_AGE) - total_received: float # 총 수령액 (만원, current_age~MAX_AGE 동일 기간 기준) + depletion_age: int | None # 자산 고갈 나이 (고갈되지 않으면 None) + depleted: bool # 자산 고갈 여부 (depletion_age is not None과 항상 일치) + total_received: WonAmountOutput # 총 수령액 (원, current_age~MAX_AGE 동일 기간 기준) break_even_age: int | None # NORMAL 대비 손익분기 나이 (이 나이 이상 생존 시 NORMAL이 유리해짐). NORMAL 자신은 None timeline: list[TimelinePoint] # 연도별 자산 추이 diff --git a/fastapi/app/services/retirement_service.py b/fastapi/app/services/retirement_service.py index 5db88d8..6308a78 100644 --- a/fastapi/app/services/retirement_service.py +++ b/fastapi/app/services/retirement_service.py @@ -1,6 +1,7 @@ import math from app.schemas.retirement import ( + Gender, ReadinessStatus, RecommendationResult, RecommendationType, @@ -31,9 +32,9 @@ MAX_DEDUCTION_YEARS = 26 # 공제일시금 선택 최대 연수 -def get_target_age(gender: str) -> int: +def get_target_age(gender: Gender) -> int: """성별에 따른 목표연령 반환""" - if gender == "male": + if gender == Gender.MALE: return TARGET_AGE_MALE return TARGET_AGE_FEMALE @@ -52,7 +53,7 @@ def simulate_retirement( monthly_expenses: float, monthly_pension: float, asset: float, - gender: str, + gender: Gender, ) -> SimulationResult: """현재 나이부터 MAX_AGE까지 매년 자산 변화를 시뮬레이션하는 공용 계산 코어. diagnosis/recommendations API는 모두 이 함수(별칭 diagnose_core)를 통해서만 시뮬레이션을 수행한다. @@ -77,10 +78,10 @@ def simulate_retirement( TimelinePoint( age=age, asset=current_asset, - income=annual_income, - expense=annual_expense, - gap=annual_gap, - cumulative_gap=cumulative_gap, + annual_income=annual_income, + annual_expense=annual_expense, + annual_gap=annual_gap, + cumulative_annual_gap=cumulative_gap, ) ) @@ -89,6 +90,9 @@ def simulate_retirement( if depletion_age is None and annual_gap > 0 and current_asset <= 0: depletion_age = age + 1 + if current_asset < 0: + current_asset = 0.0 + annual_expense *= 1 + INFLATION_RATE annual_income *= 1 + PENSION_GROWTH_RATE annual_gap = annual_expense - annual_income @@ -103,6 +107,7 @@ def simulate_retirement( current_age=current_age, monthly_gap=monthly_gap, depletion_age=depletion_age, + depleted=depletion_age is not None, target_age=target_age, status=status, timeline=timeline, @@ -152,7 +157,7 @@ def recommend_retirement( monthly_expenses: float, monthly_pension: float, asset: float, - gender: str, + gender: Gender, ) -> RecommendationResult: """MIDDLE/INSUFFICIENT 진단 시 목표연령 도달에 필요한 최소 절약액/추가소득액을 계산. @@ -177,6 +182,7 @@ def recommend_retirement( required_income=0.0, target_status=ReadinessStatus.SUFFICIENT, depletion_age=baseline.depletion_age, + depleted=baseline.depleted, target_age=baseline.target_age, status=baseline.status, timeline=baseline.timeline, @@ -221,6 +227,7 @@ def reaches_target(saving: float, extra_income: float) -> bool: required_income=required_income, target_status=ReadinessStatus.SUFFICIENT, depletion_age=improved.depletion_age, + depleted=improved.depleted, target_age=improved.target_age, status=improved.status, timeline=improved.timeline, @@ -232,7 +239,7 @@ def simulate_pension_reduction( monthly_expenses: float, monthly_pension: float, asset: float, - gender: str, + gender: Gender, reemployment_income: float, year: int | None = None, ) -> ReductionResult: @@ -265,6 +272,7 @@ def simulate_pension_reduction( reduced_monthly_pension=round(reduced_monthly_pension, 2), full_payment_income_threshold=rule.threshold, depletion_age=result.depletion_age, + depleted=result.depleted, target_age=result.target_age, status=result.status, timeline=result.timeline, @@ -324,7 +332,7 @@ def _cumulative_received(timeline: list[TimelinePoint], upfront: float) -> list[ cumulative: list[float] = [] running = upfront for point in timeline: - running += point.income + running += point.annual_income cumulative.append(running) return cumulative @@ -347,7 +355,7 @@ def simulate_scenarios( monthly_expenses: float, monthly_pension: float, asset: float, - gender: str, + gender: Gender, base_monthly_income: float, total_service_years: int, early_years: int = EARLY_YEARS_MAX, @@ -402,7 +410,6 @@ def simulate_scenarios( outcomes: list[ScenarioOutcome] = [] for scenario_type, (result, _upfront, cumulative) in simulated.items(): - depletion_age = result.depletion_age if result.depletion_age is not None else MAX_AGE total_received = cumulative[-1] if cumulative else 0.0 break_even_age = ( None @@ -413,7 +420,8 @@ def simulate_scenarios( outcomes.append( ScenarioOutcome( scenario_type=scenario_type, - depletion_age=depletion_age, + depletion_age=result.depletion_age, + depleted=result.depleted, total_received=round(total_received, 2), break_even_age=break_even_age, timeline=result.timeline, @@ -428,6 +436,12 @@ def simulate_scenarios( def _select_best_scenario(outcomes: list[ScenarioOutcome]) -> ScenarioType: - """고갈 나이가 가장 큰 시나리오를 선택하고, 동률이면 총 수령액이 큰 시나리오를 선택한다.""" - best = max(outcomes, key=lambda outcome: (outcome.depletion_age, outcome.total_received)) + """고갈 나이가 가장 큰 시나리오를 선택하고, 동률이면 총 수령액이 큰 시나리오를 선택한다. + depletion_age가 None(무고갈)인 시나리오는 가장 늦게 고갈되는 것으로 간주해 최우선한다.""" + + def sort_key(outcome: ScenarioOutcome) -> tuple[float, float]: + depletion_for_sort = outcome.depletion_age if outcome.depletion_age is not None else math.inf + return (depletion_for_sort, outcome.total_received) + + best = max(outcomes, key=sort_key) return best.scenario_type diff --git a/fastapi/data/active_income_stats.csv b/fastapi/data/active_income_stats.csv new file mode 100644 index 0000000..69d3a16 --- /dev/null +++ b/fastapi/data/active_income_stats.csv @@ -0,0 +1,8 @@ +구간,평균,중위값,p25,p75,count +0~59,3939103.5441879225,3787000.0,2817000.0,4681000.0,312427 +60~119,5274405.338985913,5082000.0,4174000.0,6234000.0,236824 +120~179,5889488.274865372,5667000.0,4654000.0,7225000.0,116246 +180~239,6582351.038499201,6413000.0,5550000.0,8094000.0,97015 +240~299,7182378.279669712,7135000.0,6338000.0,8704000.0,81868 +300~359,7626538.364426988,7740000.0,6972000.0,8704000.0,71290 +360+,8061912.59251256,8279000.0,7695000.0,8704000.0,74044 diff --git a/fastapi/requirements.txt b/fastapi/requirements.txt index 604df70..c7b6740 100644 --- a/fastapi/requirements.txt +++ b/fastapi/requirements.txt @@ -3,3 +3,4 @@ pandas==3.0.2 pydantic==2.11.7 uvicorn==0.30.0 pytest==8.3.4 +httpx==0.28.1 diff --git a/fastapi/tests/golden/_generate.py b/fastapi/tests/golden/_generate.py new file mode 100644 index 0000000..3a161b1 --- /dev/null +++ b/fastapi/tests/golden/_generate.py @@ -0,0 +1,31 @@ +"""골든 스냅샷 픽스처를 (재)생성하는 일회성 스크립트. + +주의: 이 스크립트는 "현재 동작을 있는 그대로 고정"하기 위한 것이다. +의도적으로 응답 형식을 바꾼 뒤에는 변경이 올바른지 직접 확인하고 나서만 +이 스크립트로 픽스처를 다시 생성해야 한다. 절대 무비판적으로 재실행해 +테스트를 통과시키는 용도로 쓰지 말 것. +""" + +import json +from pathlib import Path + +from fastapi.testclient import TestClient + +from app.main import app +from tests.test_golden_snapshots import CASES + +client = TestClient(app) +OUT_DIR = Path(__file__).parent + +for case in CASES: + response = client.post(case["path"], json=case["body"]) + fixture = { + "status_code": response.status_code, + "body": response.json(), + } + out_path = OUT_DIR / f"{case['name']}.json" + out_path.write_text( + json.dumps(fixture, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"wrote {out_path}") diff --git a/fastapi/tests/golden/diagnosis_insufficient.json b/fastapi/tests/golden/diagnosis_insufficient.json new file mode 100644 index 0000000..6bce3f0 --- /dev/null +++ b/fastapi/tests/golden/diagnosis_insufficient.json @@ -0,0 +1,341 @@ +{ + "body": { + "current_age": 60, + "depleted": true, + "depletion_age": 68, + "monthly_gap": 1000000, + "status": "INSUFFICIENT", + "target_age": 84, + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 12000000, + "annual_income": 18000000, + "asset": 100000000, + "cumulative_annual_gap": 12000000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 12600000, + "annual_income": 18000000, + "asset": 90640000, + "cumulative_annual_gap": 24600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 13212000, + "annual_income": 18000000, + "asset": 80381200, + "cumulative_annual_gap": 37812000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 13836240, + "annual_income": 18000000, + "asset": 69184276, + "cumulative_annual_gap": 51648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 14472965, + "annual_income": 18000000, + "asset": 57008477, + "cumulative_annual_gap": 66121205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 15122424, + "annual_income": 18000000, + "asset": 43811578, + "cumulative_annual_gap": 81243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 15784873, + "annual_income": 18000000, + "asset": 29549828, + "cumulative_annual_gap": 97028501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 16460570, + "annual_income": 18000000, + "asset": 14177904, + "cumulative_annual_gap": 113489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 17149781, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 130638853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 17852777, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 148491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 18569833, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 167061463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 19301229, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 186362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 20047254, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 206409946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 20808199, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 227218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 21584363, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 248802507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 22376050, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 271178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 23183571, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 294362129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 24007243, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 318369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 24847387, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 343216759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 25704335, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 368921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 26578422, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 395499516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 27469990, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 422969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 28379390, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 451348896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 29306978, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 480655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 30253117, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 510908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 31218180, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 542127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 32202543, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 574329715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 33206594, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 607536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 34230726, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 641767035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 35275341, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 677042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 36340848, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 713383224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 37427664, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 750810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 38536218, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 789347106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 39666942, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 829014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 40820281, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 869834329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 41996687, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 911831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 43196620, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 955027636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 44420553, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 999448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 45668964, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1045117152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 46942343, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1092059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 48241190, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1140300685 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/diagnosis_middle.json b/fastapi/tests/golden/diagnosis_middle.json new file mode 100644 index 0000000..dc6b2d2 --- /dev/null +++ b/fastapi/tests/golden/diagnosis_middle.json @@ -0,0 +1,341 @@ +{ + "body": { + "current_age": 60, + "depleted": true, + "depletion_age": 96, + "monthly_gap": 500000, + "status": "MIDDLE", + "target_age": 88, + "timeline": [ + { + "age": 60, + "annual_expense": 24000000, + "annual_gap": 6000000, + "annual_income": 18000000, + "asset": 320000000, + "cumulative_annual_gap": 6000000 + }, + { + "age": 61, + "annual_expense": 24480000, + "annual_gap": 6480000, + "annual_income": 18000000, + "asset": 323420000, + "cumulative_annual_gap": 12480000 + }, + { + "age": 62, + "annual_expense": 24969600, + "annual_gap": 6969600, + "annual_income": 18000000, + "asset": 326448200, + "cumulative_annual_gap": 19449600 + }, + { + "age": 63, + "annual_expense": 25468992, + "annual_gap": 7468992, + "annual_income": 18000000, + "asset": 329062958, + "cumulative_annual_gap": 26918592 + }, + { + "age": 64, + "annual_expense": 25978372, + "annual_gap": 7978372, + "annual_income": 18000000, + "asset": 331241785, + "cumulative_annual_gap": 34896964 + }, + { + "age": 65, + "annual_expense": 26497939, + "annual_gap": 8497939, + "annual_income": 18000000, + "asset": 332961316, + "cumulative_annual_gap": 43394903 + }, + { + "age": 66, + "annual_expense": 27027898, + "annual_gap": 9027898, + "annual_income": 18000000, + "asset": 334197278, + "cumulative_annual_gap": 52422801 + }, + { + "age": 67, + "annual_expense": 27568456, + "annual_gap": 9568456, + "annual_income": 18000000, + "asset": 334924461, + "cumulative_annual_gap": 61991257 + }, + { + "age": 68, + "annual_expense": 28119825, + "annual_gap": 10119825, + "annual_income": 18000000, + "asset": 335116685, + "cumulative_annual_gap": 72111082 + }, + { + "age": 69, + "annual_expense": 28682222, + "annual_gap": 10682222, + "annual_income": 18000000, + "asset": 334746766, + "cumulative_annual_gap": 82793304 + }, + { + "age": 70, + "annual_expense": 29255866, + "annual_gap": 11255866, + "annual_income": 18000000, + "asset": 333786480, + "cumulative_annual_gap": 94049170 + }, + { + "age": 71, + "annual_expense": 29840983, + "annual_gap": 11840983, + "annual_income": 18000000, + "asset": 332206533, + "cumulative_annual_gap": 105890153 + }, + { + "age": 72, + "annual_expense": 30437803, + "annual_gap": 12437803, + "annual_income": 18000000, + "asset": 329976516, + "cumulative_annual_gap": 118327957 + }, + { + "age": 73, + "annual_expense": 31046559, + "annual_gap": 13046559, + "annual_income": 18000000, + "asset": 327064874, + "cumulative_annual_gap": 131374516 + }, + { + "age": 74, + "annual_expense": 31667490, + "annual_gap": 13667490, + "annual_income": 18000000, + "asset": 323438864, + "cumulative_annual_gap": 145042006 + }, + { + "age": 75, + "annual_expense": 32300840, + "annual_gap": 14300840, + "annual_income": 18000000, + "asset": 319064515, + "cumulative_annual_gap": 159342846 + }, + { + "age": 76, + "annual_expense": 32946857, + "annual_gap": 14946857, + "annual_income": 18000000, + "asset": 313906585, + "cumulative_annual_gap": 174289703 + }, + { + "age": 77, + "annual_expense": 33605794, + "annual_gap": 15605794, + "annual_income": 18000000, + "asset": 307928520, + "cumulative_annual_gap": 189895497 + }, + { + "age": 78, + "annual_expense": 34277910, + "annual_gap": 16277910, + "annual_income": 18000000, + "asset": 301092408, + "cumulative_annual_gap": 206173407 + }, + { + "age": 79, + "annual_expense": 34963468, + "annual_gap": 16963468, + "annual_income": 18000000, + "asset": 293358933, + "cumulative_annual_gap": 223136875 + }, + { + "age": 80, + "annual_expense": 35662738, + "annual_gap": 17662738, + "annual_income": 18000000, + "asset": 284687329, + "cumulative_annual_gap": 240799613 + }, + { + "age": 81, + "annual_expense": 36375992, + "annual_gap": 18375992, + "annual_income": 18000000, + "asset": 275035329, + "cumulative_annual_gap": 259175605 + }, + { + "age": 82, + "annual_expense": 37103512, + "annual_gap": 19103512, + "annual_income": 18000000, + "asset": 264359117, + "cumulative_annual_gap": 278279117 + }, + { + "age": 83, + "annual_expense": 37845582, + "annual_gap": 19845582, + "annual_income": 18000000, + "asset": 252613273, + "cumulative_annual_gap": 298124699 + }, + { + "age": 84, + "annual_expense": 38602494, + "annual_gap": 20602494, + "annual_income": 18000000, + "asset": 239750721, + "cumulative_annual_gap": 318727193 + }, + { + "age": 85, + "annual_expense": 39374544, + "annual_gap": 21374544, + "annual_income": 18000000, + "asset": 225722674, + "cumulative_annual_gap": 340101737 + }, + { + "age": 86, + "annual_expense": 40162035, + "annual_gap": 22162035, + "annual_income": 18000000, + "asset": 210478574, + "cumulative_annual_gap": 362263772 + }, + { + "age": 87, + "annual_expense": 40965275, + "annual_gap": 22965275, + "annual_income": 18000000, + "asset": 193966036, + "cumulative_annual_gap": 385229047 + }, + { + "age": 88, + "annual_expense": 41784581, + "annual_gap": 23784581, + "annual_income": 18000000, + "asset": 176130783, + "cumulative_annual_gap": 409013628 + }, + { + "age": 89, + "annual_expense": 42620273, + "annual_gap": 24620273, + "annual_income": 18000000, + "asset": 156916588, + "cumulative_annual_gap": 433633901 + }, + { + "age": 90, + "annual_expense": 43472678, + "annual_gap": 25472678, + "annual_income": 18000000, + "asset": 136265205, + "cumulative_annual_gap": 459106579 + }, + { + "age": 91, + "annual_expense": 44342132, + "annual_gap": 26342132, + "annual_income": 18000000, + "asset": 114116303, + "cumulative_annual_gap": 485448711 + }, + { + "age": 92, + "annual_expense": 45228974, + "annual_gap": 27228974, + "annual_income": 18000000, + "asset": 90407396, + "cumulative_annual_gap": 512677685 + }, + { + "age": 93, + "annual_expense": 46133554, + "annual_gap": 28133554, + "annual_income": 18000000, + "asset": 65073775, + "cumulative_annual_gap": 540811238 + }, + { + "age": 94, + "annual_expense": 47056225, + "annual_gap": 29056225, + "annual_income": 18000000, + "asset": 38048428, + "cumulative_annual_gap": 569867463 + }, + { + "age": 95, + "annual_expense": 47997349, + "annual_gap": 29997349, + "annual_income": 18000000, + "asset": 9261969, + "cumulative_annual_gap": 599864812 + }, + { + "age": 96, + "annual_expense": 48957296, + "annual_gap": 30957296, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 630822109 + }, + { + "age": 97, + "annual_expense": 49936442, + "annual_gap": 31936442, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 662758551 + }, + { + "age": 98, + "annual_expense": 50935171, + "annual_gap": 32935171, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 695693722 + }, + { + "age": 99, + "annual_expense": 51953874, + "annual_gap": 33953874, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 729647596 + }, + { + "age": 100, + "annual_expense": 52992952, + "annual_gap": 34992952, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 764640548 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/diagnosis_sufficient.json b/fastapi/tests/golden/diagnosis_sufficient.json new file mode 100644 index 0000000..943937e --- /dev/null +++ b/fastapi/tests/golden/diagnosis_sufficient.json @@ -0,0 +1,301 @@ +{ + "body": { + "current_age": 65, + "depleted": false, + "depletion_age": null, + "monthly_gap": -500000, + "status": "SUFFICIENT", + "target_age": 84, + "timeline": [ + { + "age": 65, + "annual_expense": 18000000, + "annual_gap": -6000000, + "annual_income": 24000000, + "asset": 100000000, + "cumulative_annual_gap": -6000000 + }, + { + "age": 66, + "annual_expense": 18360000, + "annual_gap": -5640000, + "annual_income": 24000000, + "asset": 109180000, + "cumulative_annual_gap": -11640000 + }, + { + "age": 67, + "annual_expense": 18727200, + "annual_gap": -5272800, + "annual_income": 24000000, + "asset": 118264600, + "cumulative_annual_gap": -16912800 + }, + { + "age": 68, + "annual_expense": 19101744, + "annual_gap": -4898256, + "annual_income": 24000000, + "asset": 127243522, + "cumulative_annual_gap": -21811056 + }, + { + "age": 69, + "annual_expense": 19483779, + "annual_gap": -4516221, + "annual_income": 24000000, + "asset": 136106031, + "cumulative_annual_gap": -26327277 + }, + { + "age": 70, + "annual_expense": 19873454, + "annual_gap": -4126546, + "annual_income": 24000000, + "asset": 144840920, + "cumulative_annual_gap": -30453823 + }, + { + "age": 71, + "annual_expense": 20270924, + "annual_gap": -3729076, + "annual_income": 24000000, + "asset": 153436490, + "cumulative_annual_gap": -34182899 + }, + { + "age": 72, + "annual_expense": 20676342, + "annual_gap": -3323658, + "annual_income": 24000000, + "asset": 161880533, + "cumulative_annual_gap": -37506557 + }, + { + "age": 73, + "annual_expense": 21089869, + "annual_gap": -2910131, + "annual_income": 24000000, + "asset": 170160317, + "cumulative_annual_gap": -40416688 + }, + { + "age": 74, + "annual_expense": 21511666, + "annual_gap": -2488334, + "annual_income": 24000000, + "asset": 178262561, + "cumulative_annual_gap": -42905022 + }, + { + "age": 75, + "annual_expense": 21941900, + "annual_gap": -2058100, + "annual_income": 24000000, + "asset": 186173422, + "cumulative_annual_gap": -44963122 + }, + { + "age": 76, + "annual_expense": 22380738, + "annual_gap": -1619262, + "annual_income": 24000000, + "asset": 193878468, + "cumulative_annual_gap": -46582385 + }, + { + "age": 77, + "annual_expense": 22828352, + "annual_gap": -1171648, + "annual_income": 24000000, + "asset": 201362662, + "cumulative_annual_gap": -47754033 + }, + { + "age": 78, + "annual_expense": 23284919, + "annual_gap": -715081, + "annual_income": 24000000, + "asset": 208610339, + "cumulative_annual_gap": -48469113 + }, + { + "age": 79, + "annual_expense": 23750618, + "annual_gap": -249382, + "annual_income": 24000000, + "asset": 215605183, + "cumulative_annual_gap": -48718496 + }, + { + "age": 80, + "annual_expense": 24225630, + "annual_gap": 225630, + "annual_income": 24000000, + "asset": 222330202, + "cumulative_annual_gap": -48492865 + }, + { + "age": 81, + "annual_expense": 24710143, + "annual_gap": 710143, + "annual_income": 24000000, + "asset": 228767709, + "cumulative_annual_gap": -47782723 + }, + { + "age": 82, + "annual_expense": 25204346, + "annual_gap": 1204346, + "annual_income": 24000000, + "asset": 234899293, + "cumulative_annual_gap": -46578377 + }, + { + "age": 83, + "annual_expense": 25708432, + "annual_gap": 1708432, + "annual_income": 24000000, + "asset": 240705796, + "cumulative_annual_gap": -44869945 + }, + { + "age": 84, + "annual_expense": 26222601, + "annual_gap": 2222601, + "annual_income": 24000000, + "asset": 246167285, + "cumulative_annual_gap": -42647344 + }, + { + "age": 85, + "annual_expense": 26747053, + "annual_gap": 2747053, + "annual_income": 24000000, + "asset": 251263024, + "cumulative_annual_gap": -39900290 + }, + { + "age": 86, + "annual_expense": 27281994, + "annual_gap": 3281994, + "annual_income": 24000000, + "asset": 255971450, + "cumulative_annual_gap": -36618296 + }, + { + "age": 87, + "annual_expense": 27827634, + "annual_gap": 3827634, + "annual_income": 24000000, + "asset": 260270139, + "cumulative_annual_gap": -32790662 + }, + { + "age": 88, + "annual_expense": 28384187, + "annual_gap": 4384187, + "annual_income": 24000000, + "asset": 264135780, + "cumulative_annual_gap": -28406475 + }, + { + "age": 89, + "annual_expense": 28951870, + "annual_gap": 4951870, + "annual_income": 24000000, + "asset": 267544142, + "cumulative_annual_gap": -23454605 + }, + { + "age": 90, + "annual_expense": 29530908, + "annual_gap": 5530908, + "annual_income": 24000000, + "asset": 270470039, + "cumulative_annual_gap": -17923697 + }, + { + "age": 91, + "annual_expense": 30121526, + "annual_gap": 6121526, + "annual_income": 24000000, + "asset": 272887305, + "cumulative_annual_gap": -11802171 + }, + { + "age": 92, + "annual_expense": 30723957, + "annual_gap": 6723957, + "annual_income": 24000000, + "asset": 274768753, + "cumulative_annual_gap": -5078214 + }, + { + "age": 93, + "annual_expense": 31338436, + "annual_gap": 7338436, + "annual_income": 24000000, + "asset": 276086140, + "cumulative_annual_gap": 2260221 + }, + { + "age": 94, + "annual_expense": 31965204, + "annual_gap": 7965204, + "annual_income": 24000000, + "asset": 276810135, + "cumulative_annual_gap": 10225426 + }, + { + "age": 95, + "annual_expense": 32604509, + "annual_gap": 8604509, + "annual_income": 24000000, + "asset": 276910279, + "cumulative_annual_gap": 18829934 + }, + { + "age": 96, + "annual_expense": 33256599, + "annual_gap": 9256599, + "annual_income": 24000000, + "asset": 276354943, + "cumulative_annual_gap": 28086533 + }, + { + "age": 97, + "annual_expense": 33921731, + "annual_gap": 9921731, + "annual_income": 24000000, + "asset": 275111295, + "cumulative_annual_gap": 38008264 + }, + { + "age": 98, + "annual_expense": 34600165, + "annual_gap": 10600165, + "annual_income": 24000000, + "asset": 273145251, + "cumulative_annual_gap": 48608429 + }, + { + "age": 99, + "annual_expense": 35292169, + "annual_gap": 11292169, + "annual_income": 24000000, + "asset": 270421439, + "cumulative_annual_gap": 59900597 + }, + { + "age": 100, + "annual_expense": 35998012, + "annual_gap": 11998012, + "annual_income": 24000000, + "asset": 266903148, + "cumulative_annual_gap": 71898609 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/recommendations_saving_and_income.json b/fastapi/tests/golden/recommendations_saving_and_income.json new file mode 100644 index 0000000..9e7dd9a --- /dev/null +++ b/fastapi/tests/golden/recommendations_saving_and_income.json @@ -0,0 +1,344 @@ +{ + "body": { + "current_age": 60, + "depleted": false, + "depletion_age": null, + "recommendation_type": "SAVING_AND_INCOME", + "required_income": 2770100, + "required_saving": 1200000, + "status": "SUFFICIENT", + "target_age": 88, + "target_status": "SUFFICIENT", + "timeline": [ + { + "age": 60, + "annual_expense": 33600000, + "annual_gap": -11641200, + "annual_income": 45241200, + "asset": 50000000, + "cumulative_annual_gap": -11641200 + }, + { + "age": 61, + "annual_expense": 34272000, + "annual_gap": -10969200, + "annual_income": 45241200, + "asset": 63490436, + "cumulative_annual_gap": -22610400 + }, + { + "age": 62, + "annual_expense": 34957440, + "annual_gap": -10283760, + "annual_income": 45241200, + "asset": 76693425, + "cumulative_annual_gap": -32894160 + }, + { + "age": 63, + "annual_expense": 35656589, + "annual_gap": -9584611, + "annual_income": 45241200, + "asset": 89586501, + "cumulative_annual_gap": -42478771 + }, + { + "age": 64, + "annual_expense": 36369721, + "annual_gap": -8871479, + "annual_income": 45241200, + "asset": 102146245, + "cumulative_annual_gap": -51350251 + }, + { + "age": 65, + "annual_expense": 37097115, + "annual_gap": -8144085, + "annual_income": 45241200, + "asset": 114348256, + "cumulative_annual_gap": -59494336 + }, + { + "age": 66, + "annual_expense": 37839057, + "annual_gap": -7402143, + "annual_income": 45241200, + "asset": 126167112, + "cumulative_annual_gap": -66896478 + }, + { + "age": 67, + "annual_expense": 38595838, + "annual_gap": -6645362, + "annual_income": 45241200, + "asset": 137576332, + "cumulative_annual_gap": -73541840 + }, + { + "age": 68, + "annual_expense": 39367755, + "annual_gap": -5873445, + "annual_income": 45241200, + "asset": 148548344, + "cumulative_annual_gap": -79415285 + }, + { + "age": 69, + "annual_expense": 40155110, + "annual_gap": -5086090, + "annual_income": 45241200, + "asset": 159054443, + "cumulative_annual_gap": -84501374 + }, + { + "age": 70, + "annual_expense": 40958213, + "annual_gap": -4282987, + "annual_income": 45241200, + "asset": 169064748, + "cumulative_annual_gap": -88784362 + }, + { + "age": 71, + "annual_expense": 41777377, + "annual_gap": -3463823, + "annual_income": 45241200, + "asset": 178548168, + "cumulative_annual_gap": -92248185 + }, + { + "age": 72, + "annual_expense": 42612924, + "annual_gap": -2628276, + "annual_income": 45241200, + "asset": 187472351, + "cumulative_annual_gap": -94876461 + }, + { + "age": 73, + "annual_expense": 43465183, + "annual_gap": -1776017, + "annual_income": 45241200, + "asset": 195803646, + "cumulative_annual_gap": -96652478 + }, + { + "age": 74, + "annual_expense": 44334486, + "annual_gap": -906714, + "annual_income": 45241200, + "asset": 203507053, + "cumulative_annual_gap": -97559192 + }, + { + "age": 75, + "annual_expense": 45221176, + "annual_gap": -20024, + "annual_income": 45241200, + "asset": 210546179, + "cumulative_annual_gap": -97579215 + }, + { + "age": 76, + "annual_expense": 46125600, + "annual_gap": 884400, + "annual_income": 45241200, + "asset": 216883189, + "cumulative_annual_gap": -96694816 + }, + { + "age": 77, + "annual_expense": 47048112, + "annual_gap": 1806912, + "annual_income": 45241200, + "asset": 222478753, + "cumulative_annual_gap": -94887904 + }, + { + "age": 78, + "annual_expense": 47989074, + "annual_gap": 2747874, + "annual_income": 45241200, + "asset": 227291997, + "cumulative_annual_gap": -92140030 + }, + { + "age": 79, + "annual_expense": 48948855, + "annual_gap": 3707655, + "annual_income": 45241200, + "asset": 231280446, + "cumulative_annual_gap": -88432375 + }, + { + "age": 80, + "annual_expense": 49927833, + "annual_gap": 4686633, + "annual_income": 45241200, + "asset": 234399975, + "cumulative_annual_gap": -83745742 + }, + { + "age": 81, + "annual_expense": 50926389, + "annual_gap": 5685189, + "annual_income": 45241200, + "asset": 236604742, + "cumulative_annual_gap": -78060553 + }, + { + "age": 82, + "annual_expense": 51944917, + "annual_gap": 6703717, + "annual_income": 45241200, + "asset": 237847140, + "cumulative_annual_gap": -71356836 + }, + { + "age": 83, + "annual_expense": 52983815, + "annual_gap": 7742615, + "annual_income": 45241200, + "asset": 238077726, + "cumulative_annual_gap": -63614221 + }, + { + "age": 84, + "annual_expense": 54043492, + "annual_gap": 8802292, + "annual_income": 45241200, + "asset": 237245164, + "cumulative_annual_gap": -54811929 + }, + { + "age": 85, + "annual_expense": 55124361, + "annual_gap": 9883161, + "annual_income": 45241200, + "asset": 235296158, + "cumulative_annual_gap": -44928768 + }, + { + "age": 86, + "annual_expense": 56226849, + "annual_gap": 10985649, + "annual_income": 45241200, + "asset": 232175387, + "cumulative_annual_gap": -33943119 + }, + { + "age": 87, + "annual_expense": 57351386, + "annual_gap": 12110186, + "annual_income": 45241200, + "asset": 227825430, + "cumulative_annual_gap": -21832934 + }, + { + "age": 88, + "annual_expense": 58498413, + "annual_gap": 13257213, + "annual_income": 45241200, + "asset": 222186702, + "cumulative_annual_gap": -8575720 + }, + { + "age": 89, + "annual_expense": 59668382, + "annual_gap": 14427182, + "annual_income": 45241200, + "asset": 215197373, + "cumulative_annual_gap": 5851461 + }, + { + "age": 90, + "annual_expense": 60861749, + "annual_gap": 15620549, + "annual_income": 45241200, + "asset": 206793297, + "cumulative_annual_gap": 21472011 + }, + { + "age": 91, + "annual_expense": 62078984, + "annual_gap": 16837784, + "annual_income": 45241200, + "asset": 196907931, + "cumulative_annual_gap": 38309795 + }, + { + "age": 92, + "annual_expense": 63320564, + "annual_gap": 18079364, + "annual_income": 45241200, + "asset": 185472251, + "cumulative_annual_gap": 56389159 + }, + { + "age": 93, + "annual_expense": 64586975, + "annual_gap": 19345775, + "annual_income": 45241200, + "asset": 172414674, + "cumulative_annual_gap": 75734934 + }, + { + "age": 94, + "annual_expense": 65878715, + "annual_gap": 20637515, + "annual_income": 45241200, + "asset": 157660965, + "cumulative_annual_gap": 96372448 + }, + { + "age": 95, + "annual_expense": 67196289, + "annual_gap": 21955089, + "annual_income": 45241200, + "asset": 141134154, + "cumulative_annual_gap": 118327537 + }, + { + "age": 96, + "annual_expense": 68540215, + "annual_gap": 23299015, + "annual_income": 45241200, + "asset": 122754437, + "cumulative_annual_gap": 141626552 + }, + { + "age": 97, + "annual_expense": 69911019, + "annual_gap": 24669819, + "annual_income": 45241200, + "asset": 102439085, + "cumulative_annual_gap": 166296371 + }, + { + "age": 98, + "annual_expense": 71309239, + "annual_gap": 26068039, + "annual_income": 45241200, + "asset": 80102344, + "cumulative_annual_gap": 192364411 + }, + { + "age": 99, + "annual_expense": 72735424, + "annual_gap": 27494224, + "annual_income": 45241200, + "asset": 55655334, + "cumulative_annual_gap": 219858635 + }, + { + "age": 100, + "annual_expense": 74190133, + "annual_gap": 28948933, + "annual_income": 45241200, + "asset": 29005943, + "cumulative_annual_gap": 248807568 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/recommendations_saving_only.json b/fastapi/tests/golden/recommendations_saving_only.json new file mode 100644 index 0000000..c8fe487 --- /dev/null +++ b/fastapi/tests/golden/recommendations_saving_only.json @@ -0,0 +1,344 @@ +{ + "body": { + "current_age": 60, + "depleted": false, + "depletion_age": null, + "recommendation_type": "SAVING_ONLY", + "required_income": 0, + "required_saving": 711700, + "status": "SUFFICIENT", + "target_age": 84, + "target_status": "SUFFICIENT", + "timeline": [ + { + "age": 60, + "annual_expense": 21459600, + "annual_gap": -2540400, + "annual_income": 24000000, + "asset": 150000000, + "cumulative_annual_gap": -2540400 + }, + { + "age": 61, + "annual_expense": 21888792, + "annual_gap": -2111208, + "annual_income": 24000000, + "asset": 157116612, + "cumulative_annual_gap": -4651608 + }, + { + "age": 62, + "annual_expense": 22326568, + "annual_gap": -1673432, + "annual_income": 24000000, + "asset": 164004655, + "cumulative_annual_gap": -6325040 + }, + { + "age": 63, + "annual_expense": 22773099, + "annual_gap": -1226901, + "annual_income": 24000000, + "asset": 170648429, + "cumulative_annual_gap": -7551941 + }, + { + "age": 64, + "annual_expense": 23228561, + "annual_gap": -771439, + "annual_income": 24000000, + "asset": 177031590, + "cumulative_annual_gap": -8323380 + }, + { + "age": 65, + "annual_expense": 23693132, + "annual_gap": -306868, + "annual_income": 24000000, + "asset": 183137120, + "cumulative_annual_gap": -8630247 + }, + { + "age": 66, + "annual_expense": 24166995, + "annual_gap": 166995, + "annual_income": 24000000, + "asset": 188947307, + "cumulative_annual_gap": -8463252 + }, + { + "age": 67, + "annual_expense": 24650335, + "annual_gap": 650335, + "annual_income": 24000000, + "asset": 194443721, + "cumulative_annual_gap": -7812917 + }, + { + "age": 68, + "annual_expense": 25143342, + "annual_gap": 1143342, + "annual_income": 24000000, + "asset": 199607188, + "cumulative_annual_gap": -6669576 + }, + { + "age": 69, + "annual_expense": 25646208, + "annual_gap": 1646208, + "annual_income": 24000000, + "asset": 204417762, + "cumulative_annual_gap": -5023367 + }, + { + "age": 70, + "annual_expense": 26159133, + "annual_gap": 2159133, + "annual_income": 24000000, + "asset": 208854700, + "cumulative_annual_gap": -2864235 + }, + { + "age": 71, + "annual_expense": 26682315, + "annual_gap": 2682315, + "annual_income": 24000000, + "asset": 212896434, + "cumulative_annual_gap": -181919 + }, + { + "age": 72, + "annual_expense": 27215962, + "annual_gap": 3215962, + "annual_income": 24000000, + "asset": 216520542, + "cumulative_annual_gap": 3034042 + }, + { + "age": 73, + "annual_expense": 27760281, + "annual_gap": 3760281, + "annual_income": 24000000, + "asset": 219703718, + "cumulative_annual_gap": 6794323 + }, + { + "age": 74, + "annual_expense": 28315486, + "annual_gap": 4315486, + "annual_income": 24000000, + "asset": 222421740, + "cumulative_annual_gap": 11109810 + }, + { + "age": 75, + "annual_expense": 28881796, + "annual_gap": 4881796, + "annual_income": 24000000, + "asset": 224649442, + "cumulative_annual_gap": 15991606 + }, + { + "age": 76, + "annual_expense": 29459432, + "annual_gap": 5459432, + "annual_income": 24000000, + "asset": 226360675, + "cumulative_annual_gap": 21451038 + }, + { + "age": 77, + "annual_expense": 30048621, + "annual_gap": 6048621, + "annual_income": 24000000, + "asset": 227528280, + "cumulative_annual_gap": 27499659 + }, + { + "age": 78, + "annual_expense": 30649593, + "annual_gap": 6649593, + "annual_income": 24000000, + "asset": 228124049, + "cumulative_annual_gap": 34149252 + }, + { + "age": 79, + "annual_expense": 31262585, + "annual_gap": 7262585, + "annual_income": 24000000, + "asset": 228118689, + "cumulative_annual_gap": 41411837 + }, + { + "age": 80, + "annual_expense": 31887837, + "annual_gap": 7887837, + "annual_income": 24000000, + "asset": 227481788, + "cumulative_annual_gap": 49299674 + }, + { + "age": 81, + "annual_expense": 32525593, + "annual_gap": 8525593, + "annual_income": 24000000, + "asset": 226181769, + "cumulative_annual_gap": 57825267 + }, + { + "age": 82, + "annual_expense": 33176105, + "annual_gap": 9176105, + "annual_income": 24000000, + "asset": 224185861, + "cumulative_annual_gap": 67001372 + }, + { + "age": 83, + "annual_expense": 33839627, + "annual_gap": 9839627, + "annual_income": 24000000, + "asset": 221460048, + "cumulative_annual_gap": 76841000 + }, + { + "age": 84, + "annual_expense": 34516420, + "annual_gap": 10516420, + "annual_income": 24000000, + "asset": 217969034, + "cumulative_annual_gap": 87357420 + }, + { + "age": 85, + "annual_expense": 35206748, + "annual_gap": 11206748, + "annual_income": 24000000, + "asset": 213676192, + "cumulative_annual_gap": 98564168 + }, + { + "age": 86, + "annual_expense": 35910883, + "annual_gap": 11910883, + "annual_income": 24000000, + "asset": 208543527, + "cumulative_annual_gap": 110475052 + }, + { + "age": 87, + "annual_expense": 36629101, + "annual_gap": 12629101, + "annual_income": 24000000, + "asset": 202531623, + "cumulative_annual_gap": 123104153 + }, + { + "age": 88, + "annual_expense": 37361683, + "annual_gap": 13361683, + "annual_income": 24000000, + "asset": 195599598, + "cumulative_annual_gap": 136465836 + }, + { + "age": 89, + "annual_expense": 38108917, + "annual_gap": 14108917, + "annual_income": 24000000, + "asset": 187705052, + "cumulative_annual_gap": 150574753 + }, + { + "age": 90, + "annual_expense": 38871095, + "annual_gap": 14871095, + "annual_income": 24000000, + "asset": 178804019, + "cumulative_annual_gap": 165445848 + }, + { + "age": 91, + "annual_expense": 39648517, + "annual_gap": 15648517, + "annual_income": 24000000, + "asset": 168850912, + "cumulative_annual_gap": 181094365 + }, + { + "age": 92, + "annual_expense": 40441487, + "annual_gap": 16441487, + "annual_income": 24000000, + "asset": 157798467, + "cumulative_annual_gap": 197535852 + }, + { + "age": 93, + "annual_expense": 41250317, + "annual_gap": 17250317, + "annual_income": 24000000, + "asset": 145597689, + "cumulative_annual_gap": 214786169 + }, + { + "age": 94, + "annual_expense": 42075323, + "annual_gap": 18075323, + "annual_income": 24000000, + "asset": 132197793, + "cumulative_annual_gap": 232861492 + }, + { + "age": 95, + "annual_expense": 42916830, + "annual_gap": 18916830, + "annual_income": 24000000, + "asset": 117546144, + "cumulative_annual_gap": 251778322 + }, + { + "age": 96, + "annual_expense": 43775166, + "annual_gap": 19775166, + "annual_income": 24000000, + "asset": 101588193, + "cumulative_annual_gap": 271553489 + }, + { + "age": 97, + "annual_expense": 44650670, + "annual_gap": 20650670, + "annual_income": 24000000, + "asset": 84267418, + "cumulative_annual_gap": 292204158 + }, + { + "age": 98, + "annual_expense": 45543683, + "annual_gap": 21543683, + "annual_income": 24000000, + "asset": 65525250, + "cumulative_annual_gap": 313747841 + }, + { + "age": 99, + "annual_expense": 46454557, + "annual_gap": 22454557, + "annual_income": 24000000, + "asset": 45301014, + "cumulative_annual_gap": 336202398 + }, + { + "age": 100, + "annual_expense": 47383648, + "annual_gap": 23383648, + "annual_income": 24000000, + "asset": 23531851, + "cumulative_annual_gap": 359586046 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/recommendations_sufficient.json b/fastapi/tests/golden/recommendations_sufficient.json new file mode 100644 index 0000000..1459e68 --- /dev/null +++ b/fastapi/tests/golden/recommendations_sufficient.json @@ -0,0 +1,304 @@ +{ + "body": { + "current_age": 65, + "depleted": false, + "depletion_age": null, + "recommendation_type": "SUFFICIENT", + "required_income": 0, + "required_saving": 0, + "status": "SUFFICIENT", + "target_age": 84, + "target_status": "SUFFICIENT", + "timeline": [ + { + "age": 65, + "annual_expense": 18000000, + "annual_gap": -6000000, + "annual_income": 24000000, + "asset": 100000000, + "cumulative_annual_gap": -6000000 + }, + { + "age": 66, + "annual_expense": 18360000, + "annual_gap": -5640000, + "annual_income": 24000000, + "asset": 109180000, + "cumulative_annual_gap": -11640000 + }, + { + "age": 67, + "annual_expense": 18727200, + "annual_gap": -5272800, + "annual_income": 24000000, + "asset": 118264600, + "cumulative_annual_gap": -16912800 + }, + { + "age": 68, + "annual_expense": 19101744, + "annual_gap": -4898256, + "annual_income": 24000000, + "asset": 127243522, + "cumulative_annual_gap": -21811056 + }, + { + "age": 69, + "annual_expense": 19483779, + "annual_gap": -4516221, + "annual_income": 24000000, + "asset": 136106031, + "cumulative_annual_gap": -26327277 + }, + { + "age": 70, + "annual_expense": 19873454, + "annual_gap": -4126546, + "annual_income": 24000000, + "asset": 144840920, + "cumulative_annual_gap": -30453823 + }, + { + "age": 71, + "annual_expense": 20270924, + "annual_gap": -3729076, + "annual_income": 24000000, + "asset": 153436490, + "cumulative_annual_gap": -34182899 + }, + { + "age": 72, + "annual_expense": 20676342, + "annual_gap": -3323658, + "annual_income": 24000000, + "asset": 161880533, + "cumulative_annual_gap": -37506557 + }, + { + "age": 73, + "annual_expense": 21089869, + "annual_gap": -2910131, + "annual_income": 24000000, + "asset": 170160317, + "cumulative_annual_gap": -40416688 + }, + { + "age": 74, + "annual_expense": 21511666, + "annual_gap": -2488334, + "annual_income": 24000000, + "asset": 178262561, + "cumulative_annual_gap": -42905022 + }, + { + "age": 75, + "annual_expense": 21941900, + "annual_gap": -2058100, + "annual_income": 24000000, + "asset": 186173422, + "cumulative_annual_gap": -44963122 + }, + { + "age": 76, + "annual_expense": 22380738, + "annual_gap": -1619262, + "annual_income": 24000000, + "asset": 193878468, + "cumulative_annual_gap": -46582385 + }, + { + "age": 77, + "annual_expense": 22828352, + "annual_gap": -1171648, + "annual_income": 24000000, + "asset": 201362662, + "cumulative_annual_gap": -47754033 + }, + { + "age": 78, + "annual_expense": 23284919, + "annual_gap": -715081, + "annual_income": 24000000, + "asset": 208610339, + "cumulative_annual_gap": -48469113 + }, + { + "age": 79, + "annual_expense": 23750618, + "annual_gap": -249382, + "annual_income": 24000000, + "asset": 215605183, + "cumulative_annual_gap": -48718496 + }, + { + "age": 80, + "annual_expense": 24225630, + "annual_gap": 225630, + "annual_income": 24000000, + "asset": 222330202, + "cumulative_annual_gap": -48492865 + }, + { + "age": 81, + "annual_expense": 24710143, + "annual_gap": 710143, + "annual_income": 24000000, + "asset": 228767709, + "cumulative_annual_gap": -47782723 + }, + { + "age": 82, + "annual_expense": 25204346, + "annual_gap": 1204346, + "annual_income": 24000000, + "asset": 234899293, + "cumulative_annual_gap": -46578377 + }, + { + "age": 83, + "annual_expense": 25708432, + "annual_gap": 1708432, + "annual_income": 24000000, + "asset": 240705796, + "cumulative_annual_gap": -44869945 + }, + { + "age": 84, + "annual_expense": 26222601, + "annual_gap": 2222601, + "annual_income": 24000000, + "asset": 246167285, + "cumulative_annual_gap": -42647344 + }, + { + "age": 85, + "annual_expense": 26747053, + "annual_gap": 2747053, + "annual_income": 24000000, + "asset": 251263024, + "cumulative_annual_gap": -39900290 + }, + { + "age": 86, + "annual_expense": 27281994, + "annual_gap": 3281994, + "annual_income": 24000000, + "asset": 255971450, + "cumulative_annual_gap": -36618296 + }, + { + "age": 87, + "annual_expense": 27827634, + "annual_gap": 3827634, + "annual_income": 24000000, + "asset": 260270139, + "cumulative_annual_gap": -32790662 + }, + { + "age": 88, + "annual_expense": 28384187, + "annual_gap": 4384187, + "annual_income": 24000000, + "asset": 264135780, + "cumulative_annual_gap": -28406475 + }, + { + "age": 89, + "annual_expense": 28951870, + "annual_gap": 4951870, + "annual_income": 24000000, + "asset": 267544142, + "cumulative_annual_gap": -23454605 + }, + { + "age": 90, + "annual_expense": 29530908, + "annual_gap": 5530908, + "annual_income": 24000000, + "asset": 270470039, + "cumulative_annual_gap": -17923697 + }, + { + "age": 91, + "annual_expense": 30121526, + "annual_gap": 6121526, + "annual_income": 24000000, + "asset": 272887305, + "cumulative_annual_gap": -11802171 + }, + { + "age": 92, + "annual_expense": 30723957, + "annual_gap": 6723957, + "annual_income": 24000000, + "asset": 274768753, + "cumulative_annual_gap": -5078214 + }, + { + "age": 93, + "annual_expense": 31338436, + "annual_gap": 7338436, + "annual_income": 24000000, + "asset": 276086140, + "cumulative_annual_gap": 2260221 + }, + { + "age": 94, + "annual_expense": 31965204, + "annual_gap": 7965204, + "annual_income": 24000000, + "asset": 276810135, + "cumulative_annual_gap": 10225426 + }, + { + "age": 95, + "annual_expense": 32604509, + "annual_gap": 8604509, + "annual_income": 24000000, + "asset": 276910279, + "cumulative_annual_gap": 18829934 + }, + { + "age": 96, + "annual_expense": 33256599, + "annual_gap": 9256599, + "annual_income": 24000000, + "asset": 276354943, + "cumulative_annual_gap": 28086533 + }, + { + "age": 97, + "annual_expense": 33921731, + "annual_gap": 9921731, + "annual_income": 24000000, + "asset": 275111295, + "cumulative_annual_gap": 38008264 + }, + { + "age": 98, + "annual_expense": 34600165, + "annual_gap": 10600165, + "annual_income": 24000000, + "asset": 273145251, + "cumulative_annual_gap": 48608429 + }, + { + "age": 99, + "annual_expense": 35292169, + "annual_gap": 11292169, + "annual_income": 24000000, + "asset": 270421439, + "cumulative_annual_gap": 59900597 + }, + { + "age": 100, + "annual_expense": 35998012, + "annual_gap": 11998012, + "annual_income": 24000000, + "asset": 266903148, + "cumulative_annual_gap": 71898609 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/reduction_capped.json b/fastapi/tests/golden/reduction_capped.json new file mode 100644 index 0000000..8bc5f61 --- /dev/null +++ b/fastapi/tests/golden/reduction_capped.json @@ -0,0 +1,344 @@ +{ + "body": { + "current_age": 60, + "depleted": true, + "depletion_age": 67, + "full_payment_income_threshold": 3097000, + "monthly_reduction": 750000, + "reduced_monthly_pension": 750000, + "reemployment_income": 999990000, + "status": "INSUFFICIENT", + "target_age": 84, + "timeline": [ + { + "age": 60, + "annual_expense": 24000000, + "annual_gap": 15000000, + "annual_income": 9000000, + "asset": 100000000, + "cumulative_annual_gap": 15000000 + }, + { + "age": 61, + "annual_expense": 24480000, + "annual_gap": 15480000, + "annual_income": 9000000, + "asset": 87550000, + "cumulative_annual_gap": 30480000 + }, + { + "age": 62, + "annual_expense": 24969600, + "annual_gap": 15969600, + "annual_income": 9000000, + "asset": 74232100, + "cumulative_annual_gap": 46449600 + }, + { + "age": 63, + "annual_expense": 25468992, + "annual_gap": 16468992, + "annual_income": 9000000, + "asset": 60010375, + "cumulative_annual_gap": 62918592 + }, + { + "age": 64, + "annual_expense": 25978372, + "annual_gap": 16978372, + "annual_income": 9000000, + "asset": 44847624, + "cumulative_annual_gap": 79896964 + }, + { + "age": 65, + "annual_expense": 26497939, + "annual_gap": 17497939, + "annual_income": 9000000, + "asset": 28705330, + "cumulative_annual_gap": 97394903 + }, + { + "age": 66, + "annual_expense": 27027898, + "annual_gap": 18027898, + "annual_income": 9000000, + "asset": 11543613, + "cumulative_annual_gap": 115422801 + }, + { + "age": 67, + "annual_expense": 27568456, + "annual_gap": 18568456, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 133991257 + }, + { + "age": 68, + "annual_expense": 28119825, + "annual_gap": 19119825, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 153111082 + }, + { + "age": 69, + "annual_expense": 28682222, + "annual_gap": 19682222, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 172793304 + }, + { + "age": 70, + "annual_expense": 29255866, + "annual_gap": 20255866, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 193049170 + }, + { + "age": 71, + "annual_expense": 29840983, + "annual_gap": 20840983, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 213890153 + }, + { + "age": 72, + "annual_expense": 30437803, + "annual_gap": 21437803, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 235327957 + }, + { + "age": 73, + "annual_expense": 31046559, + "annual_gap": 22046559, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 257374516 + }, + { + "age": 74, + "annual_expense": 31667490, + "annual_gap": 22667490, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 280042006 + }, + { + "age": 75, + "annual_expense": 32300840, + "annual_gap": 23300840, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 303342846 + }, + { + "age": 76, + "annual_expense": 32946857, + "annual_gap": 23946857, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 327289703 + }, + { + "age": 77, + "annual_expense": 33605794, + "annual_gap": 24605794, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 351895497 + }, + { + "age": 78, + "annual_expense": 34277910, + "annual_gap": 25277910, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 377173407 + }, + { + "age": 79, + "annual_expense": 34963468, + "annual_gap": 25963468, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 403136875 + }, + { + "age": 80, + "annual_expense": 35662738, + "annual_gap": 26662738, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 429799613 + }, + { + "age": 81, + "annual_expense": 36375992, + "annual_gap": 27375992, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 457175605 + }, + { + "age": 82, + "annual_expense": 37103512, + "annual_gap": 28103512, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 485279117 + }, + { + "age": 83, + "annual_expense": 37845582, + "annual_gap": 28845582, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 514124699 + }, + { + "age": 84, + "annual_expense": 38602494, + "annual_gap": 29602494, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 543727193 + }, + { + "age": 85, + "annual_expense": 39374544, + "annual_gap": 30374544, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 574101737 + }, + { + "age": 86, + "annual_expense": 40162035, + "annual_gap": 31162035, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 605263772 + }, + { + "age": 87, + "annual_expense": 40965275, + "annual_gap": 31965275, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 637229047 + }, + { + "age": 88, + "annual_expense": 41784581, + "annual_gap": 32784581, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 670013628 + }, + { + "age": 89, + "annual_expense": 42620273, + "annual_gap": 33620273, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 703633901 + }, + { + "age": 90, + "annual_expense": 43472678, + "annual_gap": 34472678, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 738106579 + }, + { + "age": 91, + "annual_expense": 44342132, + "annual_gap": 35342132, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 773448711 + }, + { + "age": 92, + "annual_expense": 45228974, + "annual_gap": 36228974, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 809677685 + }, + { + "age": 93, + "annual_expense": 46133554, + "annual_gap": 37133554, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 846811238 + }, + { + "age": 94, + "annual_expense": 47056225, + "annual_gap": 38056225, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 884867463 + }, + { + "age": 95, + "annual_expense": 47997349, + "annual_gap": 38997349, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 923864812 + }, + { + "age": 96, + "annual_expense": 48957296, + "annual_gap": 39957296, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 963822109 + }, + { + "age": 97, + "annual_expense": 49936442, + "annual_gap": 40936442, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 1004758551 + }, + { + "age": 98, + "annual_expense": 50935171, + "annual_gap": 41935171, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 1046693722 + }, + { + "age": 99, + "annual_expense": 51953874, + "annual_gap": 42953874, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 1089647596 + }, + { + "age": 100, + "annual_expense": 52992952, + "annual_gap": 43992952, + "annual_income": 9000000, + "asset": 0, + "cumulative_annual_gap": 1133640548 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/reduction_no_reduction.json b/fastapi/tests/golden/reduction_no_reduction.json new file mode 100644 index 0000000..c4be98e --- /dev/null +++ b/fastapi/tests/golden/reduction_no_reduction.json @@ -0,0 +1,344 @@ +{ + "body": { + "current_age": 60, + "depleted": true, + "depletion_age": 74, + "full_payment_income_threshold": 3097000, + "monthly_reduction": 0, + "reduced_monthly_pension": 1500000, + "reemployment_income": 0, + "status": "INSUFFICIENT", + "target_age": 84, + "timeline": [ + { + "age": 60, + "annual_expense": 24000000, + "annual_gap": 6000000, + "annual_income": 18000000, + "asset": 100000000, + "cumulative_annual_gap": 6000000 + }, + { + "age": 61, + "annual_expense": 24480000, + "annual_gap": 6480000, + "annual_income": 18000000, + "asset": 96820000, + "cumulative_annual_gap": 12480000 + }, + { + "age": 62, + "annual_expense": 24969600, + "annual_gap": 6969600, + "annual_income": 18000000, + "asset": 93050200, + "cumulative_annual_gap": 19449600 + }, + { + "age": 63, + "annual_expense": 25468992, + "annual_gap": 7468992, + "annual_income": 18000000, + "asset": 88663018, + "cumulative_annual_gap": 26918592 + }, + { + "age": 64, + "annual_expense": 25978372, + "annual_gap": 7978372, + "annual_income": 18000000, + "asset": 83629847, + "cumulative_annual_gap": 34896964 + }, + { + "age": 65, + "annual_expense": 26497939, + "annual_gap": 8497939, + "annual_income": 18000000, + "asset": 77921019, + "cumulative_annual_gap": 43394903 + }, + { + "age": 66, + "annual_expense": 27027898, + "annual_gap": 9027898, + "annual_income": 18000000, + "asset": 71505772, + "cumulative_annual_gap": 52422801 + }, + { + "age": 67, + "annual_expense": 27568456, + "annual_gap": 9568456, + "annual_income": 18000000, + "asset": 64352210, + "cumulative_annual_gap": 61991257 + }, + { + "age": 68, + "annual_expense": 28119825, + "annual_gap": 10119825, + "annual_income": 18000000, + "asset": 56427267, + "cumulative_annual_gap": 72111082 + }, + { + "age": 69, + "annual_expense": 28682222, + "annual_gap": 10682222, + "annual_income": 18000000, + "asset": 47696665, + "cumulative_annual_gap": 82793304 + }, + { + "age": 70, + "annual_expense": 29255866, + "annual_gap": 11255866, + "annual_income": 18000000, + "asset": 38124877, + "cumulative_annual_gap": 94049170 + }, + { + "age": 71, + "annual_expense": 29840983, + "annual_gap": 11840983, + "annual_income": 18000000, + "asset": 27675081, + "cumulative_annual_gap": 105890153 + }, + { + "age": 72, + "annual_expense": 30437803, + "annual_gap": 12437803, + "annual_income": 18000000, + "asset": 16309121, + "cumulative_annual_gap": 118327957 + }, + { + "age": 73, + "annual_expense": 31046559, + "annual_gap": 13046559, + "annual_income": 18000000, + "asset": 3987457, + "cumulative_annual_gap": 131374516 + }, + { + "age": 74, + "annual_expense": 31667490, + "annual_gap": 13667490, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 145042006 + }, + { + "age": 75, + "annual_expense": 32300840, + "annual_gap": 14300840, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 159342846 + }, + { + "age": 76, + "annual_expense": 32946857, + "annual_gap": 14946857, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 174289703 + }, + { + "age": 77, + "annual_expense": 33605794, + "annual_gap": 15605794, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 189895497 + }, + { + "age": 78, + "annual_expense": 34277910, + "annual_gap": 16277910, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 206173407 + }, + { + "age": 79, + "annual_expense": 34963468, + "annual_gap": 16963468, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 223136875 + }, + { + "age": 80, + "annual_expense": 35662738, + "annual_gap": 17662738, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 240799613 + }, + { + "age": 81, + "annual_expense": 36375992, + "annual_gap": 18375992, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 259175605 + }, + { + "age": 82, + "annual_expense": 37103512, + "annual_gap": 19103512, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 278279117 + }, + { + "age": 83, + "annual_expense": 37845582, + "annual_gap": 19845582, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 298124699 + }, + { + "age": 84, + "annual_expense": 38602494, + "annual_gap": 20602494, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 318727193 + }, + { + "age": 85, + "annual_expense": 39374544, + "annual_gap": 21374544, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 340101737 + }, + { + "age": 86, + "annual_expense": 40162035, + "annual_gap": 22162035, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 362263772 + }, + { + "age": 87, + "annual_expense": 40965275, + "annual_gap": 22965275, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 385229047 + }, + { + "age": 88, + "annual_expense": 41784581, + "annual_gap": 23784581, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 409013628 + }, + { + "age": 89, + "annual_expense": 42620273, + "annual_gap": 24620273, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 433633901 + }, + { + "age": 90, + "annual_expense": 43472678, + "annual_gap": 25472678, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 459106579 + }, + { + "age": 91, + "annual_expense": 44342132, + "annual_gap": 26342132, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 485448711 + }, + { + "age": 92, + "annual_expense": 45228974, + "annual_gap": 27228974, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 512677685 + }, + { + "age": 93, + "annual_expense": 46133554, + "annual_gap": 28133554, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 540811238 + }, + { + "age": 94, + "annual_expense": 47056225, + "annual_gap": 29056225, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 569867463 + }, + { + "age": 95, + "annual_expense": 47997349, + "annual_gap": 29997349, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 599864812 + }, + { + "age": 96, + "annual_expense": 48957296, + "annual_gap": 30957296, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 630822109 + }, + { + "age": 97, + "annual_expense": 49936442, + "annual_gap": 31936442, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 662758551 + }, + { + "age": 98, + "annual_expense": 50935171, + "annual_gap": 32935171, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 695693722 + }, + { + "age": 99, + "annual_expense": 51953874, + "annual_gap": 33953874, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 729647596 + }, + { + "age": 100, + "annual_expense": 52992952, + "annual_gap": 34992952, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 764640548 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/reduction_partial.json b/fastapi/tests/golden/reduction_partial.json new file mode 100644 index 0000000..da26083 --- /dev/null +++ b/fastapi/tests/golden/reduction_partial.json @@ -0,0 +1,344 @@ +{ + "body": { + "current_age": 60, + "depleted": true, + "depletion_age": 72, + "full_payment_income_threshold": 3097000, + "monthly_reduction": 140300, + "reduced_monthly_pension": 1359700, + "reemployment_income": 5000000, + "status": "INSUFFICIENT", + "target_age": 84, + "timeline": [ + { + "age": 60, + "annual_expense": 24000000, + "annual_gap": 7683600, + "annual_income": 16316400, + "asset": 100000000, + "cumulative_annual_gap": 7683600 + }, + { + "age": 61, + "annual_expense": 24480000, + "annual_gap": 8163600, + "annual_income": 16316400, + "asset": 95085892, + "cumulative_annual_gap": 15847200 + }, + { + "age": 62, + "annual_expense": 24969600, + "annual_gap": 8653200, + "annual_income": 16316400, + "asset": 89529961, + "cumulative_annual_gap": 24500400 + }, + { + "age": 63, + "annual_expense": 25468992, + "annual_gap": 9152592, + "annual_income": 16316400, + "asset": 83303064, + "cumulative_annual_gap": 33652992 + }, + { + "age": 64, + "annual_expense": 25978372, + "annual_gap": 9661972, + "annual_income": 16316400, + "asset": 76374986, + "cumulative_annual_gap": 43314964 + }, + { + "age": 65, + "annual_expense": 26497939, + "annual_gap": 10181539, + "annual_income": 16316400, + "asset": 68714404, + "cumulative_annual_gap": 53496503 + }, + { + "age": 66, + "annual_expense": 27027898, + "annual_gap": 10711498, + "annual_income": 16316400, + "asset": 60288851, + "cumulative_annual_gap": 64208001 + }, + { + "age": 67, + "annual_expense": 27568456, + "annual_gap": 11252056, + "annual_income": 16316400, + "asset": 51064674, + "cumulative_annual_gap": 75460057 + }, + { + "age": 68, + "annual_expense": 28119825, + "annual_gap": 11803425, + "annual_income": 16316400, + "asset": 41006996, + "cumulative_annual_gap": 87263482 + }, + { + "age": 69, + "annual_expense": 28682222, + "annual_gap": 12365822, + "annual_income": 16316400, + "asset": 30079678, + "cumulative_annual_gap": 99629304 + }, + { + "age": 70, + "annual_expense": 29255866, + "annual_gap": 12939466, + "annual_income": 16316400, + "asset": 18245272, + "cumulative_annual_gap": 112568770 + }, + { + "age": 71, + "annual_expense": 29840983, + "annual_gap": 13524583, + "annual_income": 16316400, + "asset": 5464980, + "cumulative_annual_gap": 126093353 + }, + { + "age": 72, + "annual_expense": 30437803, + "annual_gap": 14121403, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 140214757 + }, + { + "age": 73, + "annual_expense": 31046559, + "annual_gap": 14730159, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 154944916 + }, + { + "age": 74, + "annual_expense": 31667490, + "annual_gap": 15351090, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 170296006 + }, + { + "age": 75, + "annual_expense": 32300840, + "annual_gap": 15984440, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 186280446 + }, + { + "age": 76, + "annual_expense": 32946857, + "annual_gap": 16630457, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 202910903 + }, + { + "age": 77, + "annual_expense": 33605794, + "annual_gap": 17289394, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 220200297 + }, + { + "age": 78, + "annual_expense": 34277910, + "annual_gap": 17961510, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 238161807 + }, + { + "age": 79, + "annual_expense": 34963468, + "annual_gap": 18647068, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 256808875 + }, + { + "age": 80, + "annual_expense": 35662738, + "annual_gap": 19346338, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 276155213 + }, + { + "age": 81, + "annual_expense": 36375992, + "annual_gap": 20059592, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 296214805 + }, + { + "age": 82, + "annual_expense": 37103512, + "annual_gap": 20787112, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 317001917 + }, + { + "age": 83, + "annual_expense": 37845582, + "annual_gap": 21529182, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 338531099 + }, + { + "age": 84, + "annual_expense": 38602494, + "annual_gap": 22286094, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 360817193 + }, + { + "age": 85, + "annual_expense": 39374544, + "annual_gap": 23058144, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 383875337 + }, + { + "age": 86, + "annual_expense": 40162035, + "annual_gap": 23845635, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 407720972 + }, + { + "age": 87, + "annual_expense": 40965275, + "annual_gap": 24648875, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 432369847 + }, + { + "age": 88, + "annual_expense": 41784581, + "annual_gap": 25468181, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 457838028 + }, + { + "age": 89, + "annual_expense": 42620273, + "annual_gap": 26303873, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 484141901 + }, + { + "age": 90, + "annual_expense": 43472678, + "annual_gap": 27156278, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 511298179 + }, + { + "age": 91, + "annual_expense": 44342132, + "annual_gap": 28025732, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 539323911 + }, + { + "age": 92, + "annual_expense": 45228974, + "annual_gap": 28912574, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 568236485 + }, + { + "age": 93, + "annual_expense": 46133554, + "annual_gap": 29817154, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 598053638 + }, + { + "age": 94, + "annual_expense": 47056225, + "annual_gap": 30739825, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 628793463 + }, + { + "age": 95, + "annual_expense": 47997349, + "annual_gap": 31680949, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 660474412 + }, + { + "age": 96, + "annual_expense": 48957296, + "annual_gap": 32640896, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 693115309 + }, + { + "age": 97, + "annual_expense": 49936442, + "annual_gap": 33620042, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 726735351 + }, + { + "age": 98, + "annual_expense": 50935171, + "annual_gap": 34618771, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 761354122 + }, + { + "age": 99, + "annual_expense": 51953874, + "annual_gap": 35637474, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 796991596 + }, + { + "age": 100, + "annual_expense": 52992952, + "annual_gap": 36676552, + "annual_income": 16316400, + "asset": 0, + "cumulative_annual_gap": 833668148 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/scenarios_basic.json b/fastapi/tests/golden/scenarios_basic.json new file mode 100644 index 0000000..373ea06 --- /dev/null +++ b/fastapi/tests/golden/scenarios_basic.json @@ -0,0 +1,1357 @@ +{ + "body": { + "best_scenario": "NORMAL", + "current_age": 60, + "scenarios": [ + { + "break_even_age": null, + "depleted": true, + "depletion_age": 68, + "scenario_type": "NORMAL", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 12000000, + "annual_income": 18000000, + "asset": 100000000, + "cumulative_annual_gap": 12000000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 12600000, + "annual_income": 18000000, + "asset": 90640000, + "cumulative_annual_gap": 24600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 13212000, + "annual_income": 18000000, + "asset": 80381200, + "cumulative_annual_gap": 37812000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 13836240, + "annual_income": 18000000, + "asset": 69184276, + "cumulative_annual_gap": 51648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 14472965, + "annual_income": 18000000, + "asset": 57008477, + "cumulative_annual_gap": 66121205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 15122424, + "annual_income": 18000000, + "asset": 43811578, + "cumulative_annual_gap": 81243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 15784873, + "annual_income": 18000000, + "asset": 29549828, + "cumulative_annual_gap": 97028501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 16460570, + "annual_income": 18000000, + "asset": 14177904, + "cumulative_annual_gap": 113489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 17149781, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 130638853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 17852777, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 148491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 18569833, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 167061463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 19301229, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 186362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 20047254, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 206409946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 20808199, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 227218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 21584363, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 248802507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 22376050, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 271178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 23183571, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 294362129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 24007243, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 318369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 24847387, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 343216759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 25704335, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 368921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 26578422, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 395499516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 27469990, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 422969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 28379390, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 451348896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 29306978, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 480655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 30253117, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 510908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 31218180, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 542127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 32202543, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 574329715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 33206594, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 607536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 34230726, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 641767035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 35275341, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 677042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 36340848, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 713383224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 37427664, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 750810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 38536218, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 789347106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 39666942, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 829014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 40820281, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 869834329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 41996687, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 911831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 43196620, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 955027636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 44420553, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 999448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 45668964, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1045117152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 46942343, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1092059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 48241190, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1140300685 + } + ], + "total_received": 738000000 + }, + { + "break_even_age": 60, + "depleted": true, + "depletion_age": 66, + "scenario_type": "EARLY", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 16500000, + "annual_income": 13500000, + "asset": 100000000, + "cumulative_annual_gap": 16500000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 17100000, + "annual_income": 13500000, + "asset": 86005000, + "cumulative_annual_gap": 33600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 17712000, + "annual_income": 13500000, + "asset": 70972150, + "cumulative_annual_gap": 51312000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 18336240, + "annual_income": 13500000, + "asset": 54857954, + "cumulative_annual_gap": 69648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 18972965, + "annual_income": 13500000, + "asset": 37617366, + "cumulative_annual_gap": 88621205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 19622424, + "annual_income": 13500000, + "asset": 19203733, + "cumulative_annual_gap": 108243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 20284873, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 128528501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 20960570, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 149489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 21649781, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 171138853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 22352777, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 193491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 23069833, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 216561463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 23801229, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 240362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 24547254, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 264909946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 25308199, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 290218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 26084363, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 316302507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 26876050, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 343178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 27683571, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 370862129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 28507243, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 399369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 29347387, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 428716759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 30204335, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 458921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 31078422, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 489999516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 31969990, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 521969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 32879390, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 554848896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 33806978, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 588655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 34753117, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 623408992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 35718180, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 659127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 36702543, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 695829715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 37706594, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 733536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 38730726, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 772267035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 39775341, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 812042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 40840848, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 852883224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 41927664, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 894810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 43036218, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 937847106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 44166942, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 982014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 45320281, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1027334329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 46496687, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1073831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 47696620, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1121527636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 48920553, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1170448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 50168964, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1220617152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 51442343, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1272059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 52741190, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1324800685 + } + ], + "total_received": 553500000 + }, + { + "break_even_age": 64, + "depleted": true, + "depletion_age": 67, + "scenario_type": "LUMP_SUM", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 30000000, + "annual_income": 0, + "asset": 185312500, + "cumulative_annual_gap": 30000000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 30600000, + "annual_income": 0, + "asset": 159971875, + "cumulative_annual_gap": 60600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 31212000, + "annual_income": 0, + "asset": 133253031, + "cumulative_annual_gap": 91812000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 31836240, + "annual_income": 0, + "asset": 105102262, + "cumulative_annual_gap": 123648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 32472965, + "annual_income": 0, + "asset": 75464003, + "cumulative_annual_gap": 156121205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 33122424, + "annual_income": 0, + "asset": 44280769, + "cumulative_annual_gap": 189243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 33784873, + "annual_income": 0, + "asset": 11493095, + "cumulative_annual_gap": 223028501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 34460570, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 257489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 35149781, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 292638853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 35852777, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 328491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 36569833, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 365061463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 37301229, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 402362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 38047254, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 440409946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 38808199, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 479218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 39584363, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 518802507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 40376050, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 559178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 41183571, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 600362129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 42007243, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 642369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 42847387, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 685216759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 43704335, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 728921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 44578422, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 773499516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 45469990, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 818969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 46379390, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 865348896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 47306978, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 912655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 48253117, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 960908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 49218180, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1010127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 50202543, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1060329715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 51206594, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1111536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 52230726, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1163767035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 53275341, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1217042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 54340848, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1271383224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 55427664, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1326810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 56536218, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1383347106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 57666942, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1441014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 58820281, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1499834329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 59996687, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1559831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 61196620, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1621027636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 62420553, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1683448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 63668964, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1747117152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 64942343, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1812059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 66241190, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1878300685 + } + ], + "total_received": 85312500 + }, + { + "break_even_age": 64, + "depleted": true, + "depletion_age": 67, + "scenario_type": "INSTALLMENT", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 23880000, + "annual_income": 6120000, + "asset": 148262500, + "cumulative_annual_gap": 23880000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 24480000, + "annual_income": 6120000, + "asset": 128113975, + "cumulative_annual_gap": 48360000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 25092000, + "annual_income": 6120000, + "asset": 106742994, + "cumulative_annual_gap": 73452000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 25716240, + "annual_income": 6120000, + "asset": 84100524, + "cumulative_annual_gap": 99168240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 26352965, + "annual_income": 6120000, + "asset": 60135813, + "cumulative_annual_gap": 125521205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 27002424, + "annual_income": 6120000, + "asset": 34796333, + "cumulative_annual_gap": 152523629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 27664873, + "annual_income": 6120000, + "asset": 8027726, + "cumulative_annual_gap": 180188501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 28340570, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 208529072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 29029781, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 237558853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 29732777, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 267291630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 30449833, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 297741463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 31181229, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 328922692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 31927254, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 360849946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 32688199, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 393538145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 33464363, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 427002507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 34256050, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 461258558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 35063571, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 496322129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 35887243, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 532209371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 36727387, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 568936759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 37584335, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 606521094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 38458422, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 644979516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 39349990, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 684329506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 40259390, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 724588896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 41186978, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 765775874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 42133117, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 807908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 43098180, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 851007172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 44082543, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 895089715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 45086594, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 940176309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 46110726, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 986287035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 47155341, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1033442376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 48220848, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1081663224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 49307664, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1130970888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 50416218, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1181387106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 51546942, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1232934048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 52700281, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1285634329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 53876687, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1339511016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 55076620, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1394587636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 56300553, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1450888189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 57548964, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1508437152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 58822343, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1567259495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 60121190, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1627380685 + } + ], + "total_received": 299182500 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/scenarios_with_deduction_years.json b/fastapi/tests/golden/scenarios_with_deduction_years.json new file mode 100644 index 0000000..373ea06 --- /dev/null +++ b/fastapi/tests/golden/scenarios_with_deduction_years.json @@ -0,0 +1,1357 @@ +{ + "body": { + "best_scenario": "NORMAL", + "current_age": 60, + "scenarios": [ + { + "break_even_age": null, + "depleted": true, + "depletion_age": 68, + "scenario_type": "NORMAL", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 12000000, + "annual_income": 18000000, + "asset": 100000000, + "cumulative_annual_gap": 12000000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 12600000, + "annual_income": 18000000, + "asset": 90640000, + "cumulative_annual_gap": 24600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 13212000, + "annual_income": 18000000, + "asset": 80381200, + "cumulative_annual_gap": 37812000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 13836240, + "annual_income": 18000000, + "asset": 69184276, + "cumulative_annual_gap": 51648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 14472965, + "annual_income": 18000000, + "asset": 57008477, + "cumulative_annual_gap": 66121205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 15122424, + "annual_income": 18000000, + "asset": 43811578, + "cumulative_annual_gap": 81243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 15784873, + "annual_income": 18000000, + "asset": 29549828, + "cumulative_annual_gap": 97028501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 16460570, + "annual_income": 18000000, + "asset": 14177904, + "cumulative_annual_gap": 113489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 17149781, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 130638853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 17852777, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 148491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 18569833, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 167061463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 19301229, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 186362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 20047254, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 206409946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 20808199, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 227218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 21584363, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 248802507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 22376050, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 271178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 23183571, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 294362129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 24007243, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 318369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 24847387, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 343216759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 25704335, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 368921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 26578422, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 395499516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 27469990, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 422969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 28379390, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 451348896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 29306978, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 480655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 30253117, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 510908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 31218180, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 542127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 32202543, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 574329715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 33206594, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 607536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 34230726, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 641767035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 35275341, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 677042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 36340848, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 713383224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 37427664, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 750810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 38536218, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 789347106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 39666942, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 829014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 40820281, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 869834329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 41996687, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 911831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 43196620, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 955027636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 44420553, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 999448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 45668964, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1045117152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 46942343, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1092059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 48241190, + "annual_income": 18000000, + "asset": 0, + "cumulative_annual_gap": 1140300685 + } + ], + "total_received": 738000000 + }, + { + "break_even_age": 60, + "depleted": true, + "depletion_age": 66, + "scenario_type": "EARLY", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 16500000, + "annual_income": 13500000, + "asset": 100000000, + "cumulative_annual_gap": 16500000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 17100000, + "annual_income": 13500000, + "asset": 86005000, + "cumulative_annual_gap": 33600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 17712000, + "annual_income": 13500000, + "asset": 70972150, + "cumulative_annual_gap": 51312000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 18336240, + "annual_income": 13500000, + "asset": 54857954, + "cumulative_annual_gap": 69648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 18972965, + "annual_income": 13500000, + "asset": 37617366, + "cumulative_annual_gap": 88621205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 19622424, + "annual_income": 13500000, + "asset": 19203733, + "cumulative_annual_gap": 108243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 20284873, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 128528501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 20960570, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 149489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 21649781, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 171138853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 22352777, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 193491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 23069833, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 216561463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 23801229, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 240362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 24547254, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 264909946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 25308199, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 290218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 26084363, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 316302507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 26876050, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 343178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 27683571, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 370862129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 28507243, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 399369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 29347387, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 428716759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 30204335, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 458921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 31078422, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 489999516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 31969990, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 521969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 32879390, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 554848896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 33806978, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 588655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 34753117, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 623408992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 35718180, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 659127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 36702543, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 695829715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 37706594, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 733536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 38730726, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 772267035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 39775341, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 812042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 40840848, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 852883224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 41927664, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 894810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 43036218, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 937847106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 44166942, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 982014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 45320281, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1027334329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 46496687, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1073831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 47696620, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1121527636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 48920553, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1170448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 50168964, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1220617152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 51442343, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1272059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 52741190, + "annual_income": 13500000, + "asset": 0, + "cumulative_annual_gap": 1324800685 + } + ], + "total_received": 553500000 + }, + { + "break_even_age": 64, + "depleted": true, + "depletion_age": 67, + "scenario_type": "LUMP_SUM", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 30000000, + "annual_income": 0, + "asset": 185312500, + "cumulative_annual_gap": 30000000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 30600000, + "annual_income": 0, + "asset": 159971875, + "cumulative_annual_gap": 60600000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 31212000, + "annual_income": 0, + "asset": 133253031, + "cumulative_annual_gap": 91812000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 31836240, + "annual_income": 0, + "asset": 105102262, + "cumulative_annual_gap": 123648240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 32472965, + "annual_income": 0, + "asset": 75464003, + "cumulative_annual_gap": 156121205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 33122424, + "annual_income": 0, + "asset": 44280769, + "cumulative_annual_gap": 189243629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 33784873, + "annual_income": 0, + "asset": 11493095, + "cumulative_annual_gap": 223028501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 34460570, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 257489072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 35149781, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 292638853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 35852777, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 328491630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 36569833, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 365061463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 37301229, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 402362692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 38047254, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 440409946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 38808199, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 479218145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 39584363, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 518802507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 40376050, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 559178558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 41183571, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 600362129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 42007243, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 642369371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 42847387, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 685216759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 43704335, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 728921094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 44578422, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 773499516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 45469990, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 818969506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 46379390, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 865348896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 47306978, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 912655874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 48253117, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 960908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 49218180, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1010127172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 50202543, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1060329715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 51206594, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1111536309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 52230726, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1163767035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 53275341, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1217042376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 54340848, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1271383224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 55427664, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1326810888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 56536218, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1383347106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 57666942, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1441014048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 58820281, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1499834329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 59996687, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1559831016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 61196620, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1621027636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 62420553, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1683448189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 63668964, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1747117152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 64942343, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1812059495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 66241190, + "annual_income": 0, + "asset": 0, + "cumulative_annual_gap": 1878300685 + } + ], + "total_received": 85312500 + }, + { + "break_even_age": 64, + "depleted": true, + "depletion_age": 67, + "scenario_type": "INSTALLMENT", + "timeline": [ + { + "age": 60, + "annual_expense": 30000000, + "annual_gap": 23880000, + "annual_income": 6120000, + "asset": 148262500, + "cumulative_annual_gap": 23880000 + }, + { + "age": 61, + "annual_expense": 30600000, + "annual_gap": 24480000, + "annual_income": 6120000, + "asset": 128113975, + "cumulative_annual_gap": 48360000 + }, + { + "age": 62, + "annual_expense": 31212000, + "annual_gap": 25092000, + "annual_income": 6120000, + "asset": 106742994, + "cumulative_annual_gap": 73452000 + }, + { + "age": 63, + "annual_expense": 31836240, + "annual_gap": 25716240, + "annual_income": 6120000, + "asset": 84100524, + "cumulative_annual_gap": 99168240 + }, + { + "age": 64, + "annual_expense": 32472965, + "annual_gap": 26352965, + "annual_income": 6120000, + "asset": 60135813, + "cumulative_annual_gap": 125521205 + }, + { + "age": 65, + "annual_expense": 33122424, + "annual_gap": 27002424, + "annual_income": 6120000, + "asset": 34796333, + "cumulative_annual_gap": 152523629 + }, + { + "age": 66, + "annual_expense": 33784873, + "annual_gap": 27664873, + "annual_income": 6120000, + "asset": 8027726, + "cumulative_annual_gap": 180188501 + }, + { + "age": 67, + "annual_expense": 34460570, + "annual_gap": 28340570, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 208529072 + }, + { + "age": 68, + "annual_expense": 35149781, + "annual_gap": 29029781, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 237558853 + }, + { + "age": 69, + "annual_expense": 35852777, + "annual_gap": 29732777, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 267291630 + }, + { + "age": 70, + "annual_expense": 36569833, + "annual_gap": 30449833, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 297741463 + }, + { + "age": 71, + "annual_expense": 37301229, + "annual_gap": 31181229, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 328922692 + }, + { + "age": 72, + "annual_expense": 38047254, + "annual_gap": 31927254, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 360849946 + }, + { + "age": 73, + "annual_expense": 38808199, + "annual_gap": 32688199, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 393538145 + }, + { + "age": 74, + "annual_expense": 39584363, + "annual_gap": 33464363, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 427002507 + }, + { + "age": 75, + "annual_expense": 40376050, + "annual_gap": 34256050, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 461258558 + }, + { + "age": 76, + "annual_expense": 41183571, + "annual_gap": 35063571, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 496322129 + }, + { + "age": 77, + "annual_expense": 42007243, + "annual_gap": 35887243, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 532209371 + }, + { + "age": 78, + "annual_expense": 42847387, + "annual_gap": 36727387, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 568936759 + }, + { + "age": 79, + "annual_expense": 43704335, + "annual_gap": 37584335, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 606521094 + }, + { + "age": 80, + "annual_expense": 44578422, + "annual_gap": 38458422, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 644979516 + }, + { + "age": 81, + "annual_expense": 45469990, + "annual_gap": 39349990, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 684329506 + }, + { + "age": 82, + "annual_expense": 46379390, + "annual_gap": 40259390, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 724588896 + }, + { + "age": 83, + "annual_expense": 47306978, + "annual_gap": 41186978, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 765775874 + }, + { + "age": 84, + "annual_expense": 48253117, + "annual_gap": 42133117, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 807908992 + }, + { + "age": 85, + "annual_expense": 49218180, + "annual_gap": 43098180, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 851007172 + }, + { + "age": 86, + "annual_expense": 50202543, + "annual_gap": 44082543, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 895089715 + }, + { + "age": 87, + "annual_expense": 51206594, + "annual_gap": 45086594, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 940176309 + }, + { + "age": 88, + "annual_expense": 52230726, + "annual_gap": 46110726, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 986287035 + }, + { + "age": 89, + "annual_expense": 53275341, + "annual_gap": 47155341, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1033442376 + }, + { + "age": 90, + "annual_expense": 54340848, + "annual_gap": 48220848, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1081663224 + }, + { + "age": 91, + "annual_expense": 55427664, + "annual_gap": 49307664, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1130970888 + }, + { + "age": 92, + "annual_expense": 56536218, + "annual_gap": 50416218, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1181387106 + }, + { + "age": 93, + "annual_expense": 57666942, + "annual_gap": 51546942, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1232934048 + }, + { + "age": 94, + "annual_expense": 58820281, + "annual_gap": 52700281, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1285634329 + }, + { + "age": 95, + "annual_expense": 59996687, + "annual_gap": 53876687, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1339511016 + }, + { + "age": 96, + "annual_expense": 61196620, + "annual_gap": 55076620, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1394587636 + }, + { + "age": 97, + "annual_expense": 62420553, + "annual_gap": 56300553, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1450888189 + }, + { + "age": 98, + "annual_expense": 63668964, + "annual_gap": 57548964, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1508437152 + }, + { + "age": 99, + "annual_expense": 64942343, + "annual_gap": 58822343, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1567259495 + }, + { + "age": 100, + "annual_expense": 66241190, + "annual_gap": 60121190, + "annual_income": 6120000, + "asset": 0, + "cumulative_annual_gap": 1627380685 + } + ], + "total_received": 299182500 + } + ] + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/simulate_lump_sum_only.json b/fastapi/tests/golden/simulate_lump_sum_only.json new file mode 100644 index 0000000..beb5b65 --- /dev/null +++ b/fastapi/tests/golden/simulate_lump_sum_only.json @@ -0,0 +1,13 @@ +{ + "body": { + "current_band": "0~59", + "estimated_avg_income": 6694931, + "income_factor": 1.269, + "lump_sum": 45692904, + "monthly_pension": 0, + "retire_band": "60~119", + "retire_months": 84, + "severance_pay": 7962500 + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/simulate_normal.json b/fastapi/tests/golden/simulate_normal.json new file mode 100644 index 0000000..db36118 --- /dev/null +++ b/fastapi/tests/golden/simulate_normal.json @@ -0,0 +1,13 @@ +{ + "body": { + "current_band": "240~299", + "estimated_avg_income": 5612286, + "income_factor": 0.696, + "lump_sum": 0, + "monthly_pension": 3053083, + "retire_band": "360+", + "retire_months": 384, + "severance_pay": 62400000 + }, + "status_code": 200 +} diff --git a/fastapi/tests/golden/simulate_zero_months.json b/fastapi/tests/golden/simulate_zero_months.json new file mode 100644 index 0000000..8269bc8 --- /dev/null +++ b/fastapi/tests/golden/simulate_zero_months.json @@ -0,0 +1,13 @@ +{ + "body": { + "current_band": "0~59", + "estimated_avg_income": 5000000, + "income_factor": 1.269, + "lump_sum": 0, + "monthly_pension": 0, + "retire_band": "0~59", + "retire_months": 0, + "severance_pay": 0 + }, + "status_code": 200 +} diff --git a/fastapi/tests/test_data_startup.py b/fastapi/tests/test_data_startup.py new file mode 100644 index 0000000..9e02fa5 --- /dev/null +++ b/fastapi/tests/test_data_startup.py @@ -0,0 +1,33 @@ +import pytest + +from app.repositories import employees_income_repository + + +def test_ensure_data_available_passes_when_csv_exists(): + employees_income_repository.ensure_data_available() + + +def test_ensure_data_available_raises_clear_error_when_csv_missing(monkeypatch, tmp_path): + missing_path = tmp_path / "active_income_stats.csv" + monkeypatch.setattr(employees_income_repository, "_CSV_PATH", missing_path) + + with pytest.raises(RuntimeError) as exc_info: + employees_income_repository.ensure_data_available() + + message = str(exc_info.value) + assert str(missing_path) in message + assert "active_income_stats.csv" in message + + +def test_app_startup_fails_fast_when_required_csv_missing(monkeypatch, tmp_path): + """앱 기동 시점에 검증이 실행되어, 첫 요청이 아니라 startup에서 즉시 실패해야 한다.""" + from fastapi.testclient import TestClient + + from app.main import app + + missing_path = tmp_path / "active_income_stats.csv" + monkeypatch.setattr(employees_income_repository, "_CSV_PATH", missing_path) + + with pytest.raises(RuntimeError): + with TestClient(app): + pass diff --git a/fastapi/tests/test_depletion_representation.py b/fastapi/tests/test_depletion_representation.py new file mode 100644 index 0000000..e415b26 --- /dev/null +++ b/fastapi/tests/test_depletion_representation.py @@ -0,0 +1,98 @@ +"""고갈(depletion) 표현 통일에 대한 회귀 테스트. + +- timeline의 asset은 고갈 이후 음수로 계속 내려가지 않고 0으로 클램프되어야 한다. +- ScenarioOutcome.depletion_age는 diagnosis 등 다른 응답과 마찬가지로 + 무고갈이면 None이어야 한다(더 이상 MAX_AGE(100)를 "무고갈" sentinel로 쓰지 않는다). +- depleted: bool 필드가 depletion_age is not None과 항상 일치해야 한다. +- _select_best_scenario가 depletion_age=None(무고갈) 시나리오를 다른 유한 시나리오보다 + 우선해야 한다(무고갈이 가장 좋은 결과라는 기존 의도는 유지). +""" + +import pytest + +from app.schemas.retirement import ScenarioOutcome, ScenarioType +from app.services import retirement_service +from app.services.retirement_service import MAX_AGE, simulate_retirement, simulate_scenarios + + +def test_timeline_asset_clamped_to_zero_after_depletion(): + result = simulate_retirement( + current_age=60, + monthly_expenses=250, + monthly_pension=150, + asset=10000, + gender="male", + ) + + assert result.depletion_age is not None + for point in result.timeline: + if point.age >= result.depletion_age: + assert point.asset == 0.0 + else: + assert point.asset > 0.0 + + +def test_simulation_result_depleted_flag_matches_depletion_age(): + depleting = simulate_retirement( + current_age=60, monthly_expenses=250, monthly_pension=150, asset=10000, gender="male" + ) + assert depleting.depletion_age is not None + assert depleting.depleted is True + + sufficient = simulate_retirement( + current_age=65, monthly_expenses=150, monthly_pension=200, asset=10000, gender="male" + ) + assert sufficient.depletion_age is None + assert sufficient.depleted is False + + +def test_scenario_outcome_depletion_age_is_none_when_never_depletes(): + result = simulate_scenarios( + current_age=60, + monthly_expenses=100, + monthly_pension=200, + asset=1_000_000, + gender="male", + base_monthly_income=300, + total_service_years=25, + ) + + for outcome in result.scenarios: + assert outcome.depletion_age is None, f"{outcome.scenario_type}는 무고갈이어야 하는데 {outcome.depletion_age}" + assert outcome.depleted is False + + +def test_scenario_outcome_depleted_true_when_depletes_before_max_age(): + result = simulate_scenarios( + current_age=60, + monthly_expenses=250, + monthly_pension=150, + asset=10000, + gender="male", + base_monthly_income=300, + total_service_years=25, + ) + + for outcome in result.scenarios: + assert outcome.depletion_age is not None + assert outcome.depletion_age < MAX_AGE + assert outcome.depleted is True + + +def test_select_best_scenario_prefers_never_depleting_scenario_over_finite_ones(): + outcomes = [ + ScenarioOutcome( + scenario_type=ScenarioType.NORMAL, depletion_age=80, depleted=True, + total_received=1000.0, break_even_age=None, timeline=[], + ), + ScenarioOutcome( + scenario_type=ScenarioType.EARLY, depletion_age=None, depleted=False, + total_received=500.0, break_even_age=60, timeline=[], + ), + ScenarioOutcome( + scenario_type=ScenarioType.LUMP_SUM, depletion_age=75, depleted=True, + total_received=9999.0, break_even_age=70, timeline=[], + ), + ] + + assert retirement_service._select_best_scenario(outcomes) == ScenarioType.EARLY diff --git a/fastapi/tests/test_error_format.py b/fastapi/tests/test_error_format.py new file mode 100644 index 0000000..a455e4c --- /dev/null +++ b/fastapi/tests/test_error_format.py @@ -0,0 +1,119 @@ +"""전 엔드포인트 에러 응답 포맷 통일에 대한 회귀 테스트. + +기존에는 에러가 세 가지 다른 모양으로 나갔다: + - Pydantic 422: {"detail": [{"type":..., "loc":..., "msg": 영문, ...}, ...]} + - 라우터의 HTTPException(400): {"detail": "한국어 문자열"} + - model_validator가 던진 ValueError도 422로 나가되 msg 앞에 "Value error, "가 붙음 +프론트가 error.detail을 그대로 쓸 수 없었다(문자열/배열이 상황마다 달랐음). + +이제는 모든 에러가 {"code": str, "message": str(한국어), "details": [{"field", "reason"}, ...]} +단일 포맷으로 나가야 한다. +""" + +import pytest +from fastapi.testclient import TestClient + +from app.main import app + + +@pytest.fixture(scope="module") +def client(): + return TestClient(app) + + +def _assert_unified_error_shape(body: dict) -> None: + assert set(body.keys()) == {"code", "message", "details"} + assert isinstance(body["code"], str) and body["code"] + assert isinstance(body["message"], str) and body["message"] + assert isinstance(body["details"], list) + for detail in body["details"]: + assert set(detail.keys()) == {"field", "reason"} + assert isinstance(detail["reason"], str) and detail["reason"] + + +def test_pydantic_type_error_returns_unified_shape(client): + response = client.post( + "/api/retirement/diagnosis", + json={ + "current_age": "abc", + "monthly_expenses": 2_000_000, + "monthly_pension": 1_500_000, + "asset": 100_000_000, + "gender": "male", + }, + ) + assert response.status_code == 422 + body = response.json() + _assert_unified_error_shape(body) + assert body["code"] == "VALIDATION_ERROR" + fields = {d["field"] for d in body["details"]} + assert "current_age" in fields + + +def test_business_rule_error_returns_unified_shape_with_korean_message(client): + response = client.post( + "/api/retirement/diagnosis", + json={ + "current_age": 60, + "monthly_expenses": 0, + "monthly_pension": 1_500_000, + "asset": 100_000_000, + "gender": "male", + }, + ) + assert response.status_code == 400 + body = response.json() + _assert_unified_error_shape(body) + assert body["code"] == "INVALID_INPUT" + assert "0보다 커야" in body["message"] + + +def test_model_validator_error_strips_english_value_error_prefix(client): + response = client.post( + "/api/employees/simulate", + json={ + "current_years": 20, + "current_income": 5_000_000, + "current_age": 60, + "retire_at_age": 50, + }, + ) + assert response.status_code == 422 + body = response.json() + _assert_unified_error_shape(body) + assert "Value error" not in body["message"] + assert not any("Value error" in d["reason"] for d in body["details"]) + + +def test_invalid_gender_enum_returns_unified_shape(client): + response = client.post( + "/api/retirement/diagnosis", + json={ + "current_age": 60, + "monthly_expenses": 2_000_000, + "monthly_pension": 1_500_000, + "asset": 100_000_000, + "gender": "MALE", + }, + ) + assert response.status_code == 422 + body = response.json() + _assert_unified_error_shape(body) + fields = {d["field"] for d in body["details"]} + assert "gender" in fields + + +@pytest.mark.parametrize( + "path,body", + [ + ("/api/retirement/diagnosis", {"current_age": "abc"}), + ("/api/retirement/recommendations", {"current_age": "abc"}), + ("/api/retirement/reduction", {"current_age": "abc"}), + ("/api/retirement/scenarios", {"current_age": "abc"}), + ("/api/employees/simulate", {"current_age": "abc"}), + ], +) +def test_all_endpoints_share_same_error_envelope(client, path, body): + response = client.post(path, json=body) + assert response.status_code == 422 + _assert_unified_error_shape(response.json()) diff --git a/fastapi/tests/test_gender_validation.py b/fastapi/tests/test_gender_validation.py new file mode 100644 index 0000000..dbeed1c --- /dev/null +++ b/fastapi/tests/test_gender_validation.py @@ -0,0 +1,45 @@ +"""gender 필드 검증에 대한 회귀 테스트. + +기존에는 gender: str에 아무 제약이 없어, "MALE"(대문자 오타) 같은 값이 +get_target_age()의 `if gender == "male": ... else: FEMALE` 분기를 타고 +조용히 여성 목표연령(88세)으로 처리됐다(HTTP 200). 이제는 "male"/"female" +외의 값은 422로 거부되어야 한다. +""" + +import pytest +from fastapi.testclient import TestClient + +from app.main import app + + +@pytest.fixture(scope="module") +def client(): + return TestClient(app) + + +def _diagnosis_body(gender) -> dict: + return { + "current_age": 60, + "monthly_expenses": 2_000_000, + "monthly_pension": 1_500_000, + "asset": 100_000_000, + "gender": gender, + } + + +def test_diagnosis_accepts_male(client): + response = client.post("/api/retirement/diagnosis", json=_diagnosis_body("male")) + assert response.status_code == 200 + assert response.json()["target_age"] == 84 + + +def test_diagnosis_accepts_female(client): + response = client.post("/api/retirement/diagnosis", json=_diagnosis_body("female")) + assert response.status_code == 200 + assert response.json()["target_age"] == 88 + + +@pytest.mark.parametrize("invalid_gender", ["MALE", "Male", "m", "남성", "unknown", ""]) +def test_diagnosis_rejects_invalid_gender_with_422(client, invalid_gender): + response = client.post("/api/retirement/diagnosis", json=_diagnosis_body(invalid_gender)) + assert response.status_code == 422 diff --git a/fastapi/tests/test_golden_snapshots.py b/fastapi/tests/test_golden_snapshots.py new file mode 100644 index 0000000..e1ace58 --- /dev/null +++ b/fastapi/tests/test_golden_snapshots.py @@ -0,0 +1,206 @@ +"""5개 엔드포인트의 현재(수정 전) 응답을 고정하는 골든 스냅샷 테스트. + +이후 이슈 수정 작업에서 의도한 필드 외의 값이 조금이라도 바뀌면 이 테스트가 +즉시 실패해야 한다. 산식/응답 형식을 의도적으로 바꾼 뒤에는: + 1) 새 응답이 의도한 변경과 정확히 일치하는지 직접 확인하고 + 2) tests/golden/_generate.py로 해당 케이스의 픽스처만 다시 생성한다. +케이스를 통과시키기 위해 무비판적으로 재생성하지 말 것. +""" + +import json +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from app.main import app + +GOLDEN_DIR = Path(__file__).parent / "golden" + +# retirement 계열 엔드포인트는 #1(금액 단위 통일)로 요청/응답이 만원 -> 원으로 바뀌었다. +# 케이스가 나타내는 실제 시나리오(예: "월 생활비 250만원")는 그대로 유지하기 위해 +# 만원 단위였던 리터럴에 WON_PER_MANWON을 곱해 원 단위로 표현한다. +WON_PER_MANWON = 10_000 + +CASES = [ + # --- /api/retirement/diagnosis --- + { + "name": "diagnosis_sufficient", + "path": "/api/retirement/diagnosis", + "body": { + "current_age": 65, + "monthly_expenses": 150 * WON_PER_MANWON, + "monthly_pension": 200 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + }, + }, + { + "name": "diagnosis_insufficient", + "path": "/api/retirement/diagnosis", + "body": { + "current_age": 60, + "monthly_expenses": 250 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + }, + }, + { + "name": "diagnosis_middle", + "path": "/api/retirement/diagnosis", + "body": { + "current_age": 60, + "monthly_expenses": 200 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 32000 * WON_PER_MANWON, + "gender": "female", + }, + }, + # --- /api/retirement/recommendations --- + { + "name": "recommendations_sufficient", + "path": "/api/retirement/recommendations", + "body": { + "current_age": 65, + "monthly_expenses": 150 * WON_PER_MANWON, + "monthly_pension": 200 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + }, + }, + { + "name": "recommendations_saving_only", + "path": "/api/retirement/recommendations", + "body": { + "current_age": 60, + "monthly_expenses": 250 * WON_PER_MANWON, + "monthly_pension": 200 * WON_PER_MANWON, + "asset": 15000 * WON_PER_MANWON, + "gender": "male", + }, + }, + { + "name": "recommendations_saving_and_income", + "path": "/api/retirement/recommendations", + "body": { + "current_age": 60, + "monthly_expenses": 400 * WON_PER_MANWON, + "monthly_pension": 100 * WON_PER_MANWON, + "asset": 5000 * WON_PER_MANWON, + "gender": "female", + }, + }, + # --- /api/retirement/reduction --- + { + "name": "reduction_no_reduction", + "path": "/api/retirement/reduction", + "body": { + "current_age": 60, + "monthly_expenses": 200 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + "reemployment_income": 0, + }, + }, + { + "name": "reduction_partial", + "path": "/api/retirement/reduction", + "body": { + "current_age": 60, + "monthly_expenses": 200 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + "reemployment_income": 500 * WON_PER_MANWON, + }, + }, + { + "name": "reduction_capped", + "path": "/api/retirement/reduction", + "body": { + "current_age": 60, + "monthly_expenses": 200 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + "reemployment_income": 99999 * WON_PER_MANWON, + }, + }, + # --- /api/retirement/scenarios --- + { + "name": "scenarios_basic", + "path": "/api/retirement/scenarios", + "body": { + "current_age": 60, + "monthly_expenses": 250 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + "base_monthly_income": 300 * WON_PER_MANWON, + "total_service_years": 25, + }, + }, + { + "name": "scenarios_with_deduction_years", + "path": "/api/retirement/scenarios", + "body": { + "current_age": 60, + "monthly_expenses": 250 * WON_PER_MANWON, + "monthly_pension": 150 * WON_PER_MANWON, + "asset": 10000 * WON_PER_MANWON, + "gender": "male", + "base_monthly_income": 300 * WON_PER_MANWON, + "total_service_years": 25, + "deduction_years": 15, + }, + }, + # --- /api/employees/simulate --- + { + "name": "simulate_normal", + "path": "/api/employees/simulate", + "body": { + "current_years": 20, + "current_income": 5000000, + "current_age": 50, + "retire_at_age": 62, + }, + }, + { + "name": "simulate_lump_sum_only", + "path": "/api/employees/simulate", + "body": { + "current_years": 2, + "current_income": 5000000, + "current_age": 30, + "retire_at_age": 35, + }, + }, + { + "name": "simulate_zero_months", + "path": "/api/employees/simulate", + "body": { + "current_years": 0, + "current_income": 5000000, + "current_age": 30, + "retire_at_age": 30, + }, + }, +] + + +@pytest.fixture(scope="module") +def client(): + return TestClient(app) + + +@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES]) +def test_golden_snapshot(client, case): + fixture_path = GOLDEN_DIR / f"{case['name']}.json" + expected = json.loads(fixture_path.read_text(encoding="utf-8")) + + response = client.post(case["path"], json=case["body"]) + + assert response.status_code == expected["status_code"] + assert response.json() == expected["body"] diff --git a/fastapi/tests/test_money_unit_consistency.py b/fastapi/tests/test_money_unit_consistency.py new file mode 100644 index 0000000..f89a312 --- /dev/null +++ b/fastapi/tests/test_money_unit_consistency.py @@ -0,0 +1,80 @@ +"""API 경계의 금액 단위(원) 일관성 검증. + +simulate의 monthly_pension 출력을 diagnosis의 monthly_pension 입력에 그대로 +넣었을 때, 두 API가 같은 단위(원)를 쓰고 있다면 diagnosis의 monthly_gap이 +(입력한 monthly_expenses - monthly_pension)과 정확히 일치해야 한다. +단위가 어긋나 있으면(예: 한쪽 원, 한쪽 만원) 이 값이 10,000배 가까이 벌어진다. +""" + +import pytest +from fastapi.testclient import TestClient + +from app.main import app + + +@pytest.fixture(scope="module") +def client(): + return TestClient(app) + + +def test_simulate_monthly_pension_feeds_consistently_into_diagnosis(client): + simulate_response = client.post( + "/api/employees/simulate", + json={ + "current_years": 20, + "current_income": 5_000_000, + "current_age": 50, + "retire_at_age": 62, + }, + ) + assert simulate_response.status_code == 200 + monthly_pension_won = simulate_response.json()["monthly_pension"] + assert monthly_pension_won > 100_000 # 원 단위라면 이 정도 규모가 정상 + + monthly_expenses_won = monthly_pension_won + 500_000 + + diagnosis_response = client.post( + "/api/retirement/diagnosis", + json={ + "current_age": 62, + "monthly_expenses": monthly_expenses_won, + "monthly_pension": monthly_pension_won, + "asset": 100_000_000, + "gender": "male", + }, + ) + assert diagnosis_response.status_code == 200 + body = diagnosis_response.json() + + assert body["monthly_gap"] == pytest.approx( + monthly_expenses_won - monthly_pension_won, abs=2 + ) + + +def test_reduction_income_at_threshold_produces_zero_reduction(client): + """reduction_rules.threshold(309.7만원, 고정 상수)는 입력값 스케일과 무관하게 + 고정돼 있으므로, reemployment_income을 '원' 단위로 넣었을 때 내부 변환이 + 빠지면 이 값이 threshold를 훨씬 초과한 것처럼 오인되어 감액이 발생한다. + threshold와 정확히 같은 소득(원 단위)을 넣으면 감액이 0이어야 한다. + """ + from app.services.reduction_rules import get_reduction_rule + + threshold_manwon = get_reduction_rule(2025).threshold + reemployment_income_won = round(threshold_manwon * 10_000) + + response = client.post( + "/api/retirement/reduction", + json={ + "current_age": 60, + "monthly_expenses": 2_000_000, + "monthly_pension": 1_500_000, + "asset": 100_000_000, + "gender": "male", + "reemployment_income": reemployment_income_won, + "year": 2025, + }, + ) + + assert response.status_code == 200 + body = response.json() + assert body["monthly_reduction"] == 0 diff --git a/fastapi/tests/test_retirement_service.py b/fastapi/tests/test_retirement_service.py index 0a72cb5..5a75c20 100644 --- a/fastapi/tests/test_retirement_service.py +++ b/fastapi/tests/test_retirement_service.py @@ -97,7 +97,7 @@ def test_simulate_retirement_gap_matches_expense_minus_income_when_pension_grows ) for point in result.timeline: - assert point.gap == pytest.approx(point.expense - point.income) + assert point.annual_gap == pytest.approx(point.annual_expense - point.annual_income) def test_simulate_retirement_depletion_age_capped_at_max_age(): @@ -381,15 +381,15 @@ def test_simulate_scenarios_timeline_matches_diagnosis_core_format(): def test_select_best_scenario_breaks_tie_by_total_received(): outcomes = [ ScenarioOutcome( - scenario_type=ScenarioType.NORMAL, depletion_age=80, total_received=1000.0, + scenario_type=ScenarioType.NORMAL, depletion_age=80, depleted=True, total_received=1000.0, break_even_age=None, timeline=[], ), ScenarioOutcome( - scenario_type=ScenarioType.EARLY, depletion_age=80, total_received=1500.0, + scenario_type=ScenarioType.EARLY, depletion_age=80, depleted=True, total_received=1500.0, break_even_age=None, timeline=[], ), ScenarioOutcome( - scenario_type=ScenarioType.LUMP_SUM, depletion_age=75, total_received=9999.0, + scenario_type=ScenarioType.LUMP_SUM, depletion_age=75, depleted=True, total_received=9999.0, break_even_age=70, timeline=[], ), ] @@ -436,8 +436,8 @@ def test_simulate_scenarios_lump_sum_break_even_age_is_consistent_with_cumulativ normal_running = 0.0 lump_running = upfront for n_point, l_point in zip(normal_outcome.timeline, lump_sum_outcome.timeline): - normal_running += n_point.income - lump_running += l_point.income + normal_running += n_point.annual_income + lump_running += l_point.annual_income if n_point.age < lump_sum_outcome.break_even_age: assert normal_running < lump_running elif n_point.age == lump_sum_outcome.break_even_age: @@ -486,7 +486,7 @@ def test_simulate_scenarios_early_reduction_matches_per_year_rate(): early_outcome = next(o for o in result.scenarios if o.scenario_type == ScenarioType.EARLY) expected_monthly_income = monthly_pension * (1 - EARLY_REDUCTION_RATE_PER_YEAR * 2) * 12 - assert early_outcome.timeline[0].income == pytest.approx(expected_monthly_income) + assert early_outcome.timeline[0].annual_income == pytest.approx(expected_monthly_income) def test_calculate_lump_sum_and_pension_matches_manual_example(): @@ -598,8 +598,8 @@ def test_simulate_scenarios_lump_sum_and_installment_use_unified_formula(): upfront_lump = lump_sum_outcome.timeline[0].asset - 10_000 assert upfront_lump == pytest.approx(expected_full_lump_sum) - assert lump_sum_outcome.timeline[0].income == pytest.approx(expected_full_pension * 12) + assert lump_sum_outcome.timeline[0].annual_income == pytest.approx(expected_full_pension * 12) upfront_installment = installment_outcome.timeline[0].asset - 10_000 assert upfront_installment == pytest.approx(expected_split_lump_sum) - assert installment_outcome.timeline[0].income == pytest.approx(expected_split_pension * 12) + assert installment_outcome.timeline[0].annual_income == pytest.approx(expected_split_pension * 12)