import uuid from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.db.models.user import User from app.schemas.tax import ( ManualDisposalCreate, ManualDisposalResponse, ManualDisposalUpdate, P60Entry, PayslipCreate, PayslipResponse, PayslipUpdate, TaxProfileCreate, TaxProfileResponse, TaxRateConfigResponse, TaxRateConfigUpdate, TaxReportResponse, ) from app.services import tax_service router = APIRouter(tags=["tax"]) # --------------------------------------------------------------------------- # Rate configs # --------------------------------------------------------------------------- @router.get("/tax/rate-configs", response_model=list[int]) async def list_rate_config_years( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): return await tax_service.list_configured_years(db, current_user.id) @router.get("/tax/rate-configs/{tax_year}", response_model=TaxRateConfigResponse) async def get_rate_config( tax_year: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: return await tax_service.get_rate_config(db, current_user.id, tax_year) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.put("/tax/rate-configs/{tax_year}", response_model=TaxRateConfigResponse) async def upsert_rate_config( tax_year: int, data: TaxRateConfigUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): rates = {k: v for k, v in data.model_dump().items() if v is not None} if not rates: raise HTTPException(status_code=422, detail="At least one rate type must be provided") result = await tax_service.upsert_rate_config(db, current_user.id, tax_year, rates) await db.commit() return result # --------------------------------------------------------------------------- # Tax profile # --------------------------------------------------------------------------- @router.get("/tax/profile/{tax_year}", response_model=TaxProfileResponse) async def get_tax_profile( tax_year: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): profile = await tax_service.get_tax_profile(db, current_user.id, tax_year) if profile is None: raise HTTPException(status_code=404, detail="No tax profile for this year") return tax_service._profile_to_response(profile) @router.put("/tax/profile/{tax_year}", response_model=TaxProfileResponse) async def upsert_tax_profile( tax_year: int, data: TaxProfileCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): profile = await tax_service.upsert_tax_profile( db, current_user.id, tax_year, tax_code=data.tax_code, employer_name=data.employer_name, is_cumulative=data.is_cumulative, ) await db.commit() return tax_service._profile_to_response(profile) # --------------------------------------------------------------------------- # Payslips # --------------------------------------------------------------------------- @router.get("/tax/payslips/{tax_year}", response_model=list[PayslipResponse]) async def list_payslips( tax_year: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): payslips = await tax_service.list_payslips(db, current_user.id, tax_year) return [tax_service._payslip_to_response(p) for p in payslips] @router.post("/tax/payslips/{tax_year}", response_model=PayslipResponse, status_code=status.HTTP_201_CREATED) async def create_payslip( tax_year: int, data: PayslipCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: payslip = await tax_service.create_payslip( db, current_user.id, tax_year, period_month=data.period_month, period_year=data.period_year, gross_pay=data.gross_pay, income_tax_withheld=data.income_tax_withheld, ni_withheld=data.ni_withheld, net_pay=data.net_pay, notes=data.notes, ) await db.commit() return tax_service._payslip_to_response(payslip) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.put("/tax/payslips/{payslip_id}", response_model=PayslipResponse) async def update_payslip( payslip_id: uuid.UUID, data: PayslipUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: updates = {k: v for k, v in data.model_dump().items() if v is not None} payslip = await tax_service.update_payslip(db, current_user.id, payslip_id, **updates) await db.commit() return tax_service._payslip_to_response(payslip) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.delete("/tax/payslips/{payslip_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_payslip( payslip_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: await tax_service.delete_payslip(db, current_user.id, payslip_id) await db.commit() except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.post("/tax/payslips/{tax_year}/p60", status_code=status.HTTP_204_NO_CONTENT) async def enter_p60( tax_year: int, data: P60Entry, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: await tax_service.replace_with_p60( db, current_user.id, tax_year, gross_pay=data.gross_pay, income_tax_withheld=data.income_tax_withheld, ni_withheld=data.ni_withheld, net_pay=data.net_pay, ) await db.commit() except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) # --------------------------------------------------------------------------- # Manual CGT disposals # --------------------------------------------------------------------------- @router.get("/tax/cgt-disposals/{tax_year}", response_model=list[ManualDisposalResponse]) async def list_cgt_disposals( tax_year: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): disposals = await tax_service.list_manual_disposals(db, current_user.id, tax_year) return [tax_service._disposal_to_response(d) for d in disposals] @router.post("/tax/cgt-disposals/{tax_year}", response_model=ManualDisposalResponse, status_code=status.HTTP_201_CREATED) async def create_cgt_disposal( tax_year: int, data: ManualDisposalCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): disposal = await tax_service.create_manual_disposal( db, current_user.id, tax_year, disposal_date=data.disposal_date, asset_description=data.asset_description, proceeds=data.proceeds, cost_basis=data.cost_basis, notes=data.notes, ) await db.commit() return tax_service._disposal_to_response(disposal) @router.put("/tax/cgt-disposals/{disposal_id}", response_model=ManualDisposalResponse) async def update_cgt_disposal( disposal_id: uuid.UUID, data: ManualDisposalUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): from sqlalchemy import select from app.db.models.tax import ManualCGTDisposal from app.core.security import decrypt_field result = await db.execute( select(ManualCGTDisposal).where( ManualCGTDisposal.id == disposal_id, ManualCGTDisposal.user_id == current_user.id, ) ) disposal = result.scalar_one_or_none() if disposal is None: raise HTTPException(status_code=404, detail="Disposal not found") current_desc = decrypt_field(disposal.asset_description_enc) if disposal.asset_description_enc else "" try: updated = await tax_service.update_manual_disposal( db, current_user.id, disposal_id, disposal_date=data.disposal_date or disposal.disposal_date, asset_description=data.asset_description or current_desc, proceeds=data.proceeds if data.proceeds is not None else disposal.proceeds, cost_basis=data.cost_basis if data.cost_basis is not None else disposal.cost_basis, notes=data.notes, ) await db.commit() return tax_service._disposal_to_response(updated) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @router.delete("/tax/cgt-disposals/{disposal_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_cgt_disposal( disposal_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: await tax_service.delete_manual_disposal(db, current_user.id, disposal_id) await db.commit() except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) # --------------------------------------------------------------------------- # Tax report # --------------------------------------------------------------------------- @router.get("/tax/report/{tax_year}", response_model=TaxReportResponse) async def get_tax_report( tax_year: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: return await tax_service.build_tax_report(db, current_user.id, tax_year) except ValueError as e: raise HTTPException(status_code=404, detail=str(e))