""" 賞与計算API (Bonus Calculation API) """ from fastapi import APIRouter, HTTPException, status from typing import List from datetime import datetime from . import schemas from .service import BonusCalculationService from ...core.database import get_connection router = APIRouter(prefix="/payroll/bonus", tags=["Payroll - Bonus"]) @router.post("/calculate", response_model=schemas.BonusPayment, status_code=status.HTTP_201_CREATED) def calculate_bonus(request: schemas.BonusCalculationRequest): """賞与を計算""" # 賞与を計算 calc_result = BonusCalculationService.calculate_bonus( employee_id=request.employee_id, bonus_year=request.bonus_year, bonus_month=request.bonus_month, bonus_type=request.bonus_type, payment_date=request.payment_date, base_bonus=request.base_bonus, performance_bonus=request.performance_bonus, other_bonus=request.other_bonus, other_deduction=request.other_deduction, calculated_by=request.calculated_by ) # データベースに保存 with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ INSERT INTO bonus_payments ( employee_id, bonus_year, bonus_month, bonus_type, payment_date, status, base_bonus, performance_bonus, other_bonus, total_bonus, health_insurance, care_insurance, pension_insurance, employment_insurance, child_support, income_tax, other_deduction, total_deduction, net_bonus, calculated_at, calculated_by ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) RETURNING * """, ( calc_result["employee_id"], calc_result["bonus_year"], calc_result["bonus_month"], calc_result["bonus_type"], calc_result["payment_date"], calc_result["status"], calc_result["base_bonus"], calc_result["performance_bonus"], calc_result["other_bonus"], calc_result["total_bonus"], calc_result["health_insurance"], calc_result["care_insurance"], calc_result["pension_insurance"], calc_result["employment_insurance"], calc_result["child_support"], calc_result["income_tax"], calc_result["other_deduction"], calc_result["total_deduction"], calc_result["net_bonus"], datetime.now(), calc_result["calculated_by"] ) ) result = cur.fetchone() conn.commit() return result @router.get("/", response_model=List[schemas.BonusPayment]) def get_bonuses(bonus_year: int = None, employee_id: int = None): """賞与一覧を取得""" conditions = [] params = [] if bonus_year: conditions.append("bonus_year = %s") params.append(bonus_year) if employee_id: conditions.append("employee_id = %s") params.append(employee_id) where_clause = " AND ".join(conditions) if conditions else "1=1" with get_connection() as conn: with conn.cursor() as cur: cur.execute( f""" SELECT * FROM bonus_payments WHERE {where_clause} ORDER BY bonus_year DESC, bonus_month DESC """, params ) return cur.fetchall() @router.get("/{bonus_id}", response_model=schemas.BonusPayment) def get_bonus(bonus_id: int): """賞与詳細を取得""" with get_connection() as conn: with conn.cursor() as cur: cur.execute("SELECT * FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="賞与が見つかりません") return result @router.post("/{bonus_id}/approve", response_model=schemas.BonusPayment) def approve_bonus(bonus_id: int, approved_by: str): """賞与を承認""" with get_connection() as conn: with conn.cursor() as cur: cur.execute( """ UPDATE bonus_payments SET status = 'approved', approved_at = %s, approved_by = %s WHERE bonus_id = %s AND status = 'calculated' RETURNING * """, (datetime.now(), approved_by, bonus_id) ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="賞与が見つかりません、または既に承認済みです") conn.commit() return result @router.put("/{bonus_id}", response_model=schemas.BonusPayment) def update_bonus(bonus_id: int, request: schemas.BonusUpdateRequest): """賞与の支給項目を更新(再計算は別途実行)""" with get_connection() as conn: with conn.cursor() as cur: cur.execute("SELECT status FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) row = cur.fetchone() if not row: raise HTTPException(status_code=404, detail="賞与が見つかりません") if row["status"] == "approved": raise HTTPException(status_code=400, detail="承認済みの賞与は編集できません") cur.execute( """ UPDATE bonus_payments SET base_bonus = %s, performance_bonus = %s, other_bonus = %s, other_deduction = %s, payment_date = COALESCE(%s, payment_date), bonus_type = COALESCE(%s, bonus_type) WHERE bonus_id = %s RETURNING * """, ( request.base_bonus, request.performance_bonus, request.other_bonus, request.other_deduction, request.payment_date, request.bonus_type, bonus_id ) ) result = cur.fetchone() conn.commit() return result @router.post("/{bonus_id}/recalculate", response_model=schemas.BonusPayment) def recalculate_bonus(bonus_id: int): """賞与を再計算""" with get_connection() as conn: with conn.cursor() as cur: # 既存データを取得 cur.execute("SELECT * FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) existing = cur.fetchone() if not existing: raise HTTPException(status_code=404, detail="賞与データが見つかりません") if existing["status"] == "approved": raise HTTPException(status_code=400, detail="承認済みの賞与は再計算できません") # 再計算 try: calc_result = BonusCalculationService.calculate_bonus( employee_id=existing["employee_id"], bonus_year=existing["bonus_year"], bonus_month=existing["bonus_month"], bonus_type=existing["bonus_type"], payment_date=existing["payment_date"], base_bonus=existing["base_bonus"], performance_bonus=existing["performance_bonus"], other_bonus=existing["other_bonus"], other_deduction=existing["other_deduction"], calculated_by="recalculated" ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) # 更新 cur.execute( """ UPDATE bonus_payments SET base_bonus = %s, performance_bonus = %s, other_bonus = %s, total_bonus = %s, health_insurance = %s, care_insurance = %s, pension_insurance = %s, employment_insurance = %s, child_support = %s, income_tax = %s, other_deduction = %s, total_deduction = %s, net_bonus = %s, calculated_at = %s, calculated_by = %s WHERE bonus_id = %s RETURNING * """, ( calc_result["base_bonus"], calc_result["performance_bonus"], calc_result["other_bonus"], calc_result["total_bonus"], calc_result["health_insurance"], calc_result["care_insurance"], calc_result["pension_insurance"], calc_result["employment_insurance"], calc_result["child_support"], calc_result["income_tax"], calc_result["other_deduction"], calc_result["total_deduction"], calc_result["net_bonus"], datetime.now(), "recalculated", bonus_id ) ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="賞与を更新できません") conn.commit() return result @router.delete("/{bonus_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_bonus(bonus_id: int): """賞与を削除""" with get_connection() as conn: with conn.cursor() as cur: # 承認済みは削除不可 cur.execute("SELECT status FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) row = cur.fetchone() if not row: raise HTTPException(status_code=404, detail="賞与が見つかりません、または削除できません") if row["status"] == "approved": raise HTTPException(status_code=400, detail="承認済みの賞与は削除できません") cur.execute("DELETE FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) conn.commit()