2026-06-20 03:29:51 +02:00
|
|
|
from typing import Optional
|
|
|
|
|
|
2026-07-03 20:20:22 +02:00
|
|
|
from fastapi import APIRouter, File, Query, Request, UploadFile
|
2026-06-20 03:29:51 +02:00
|
|
|
|
|
|
|
|
from app.modules.also.backend.service import also_service
|
|
|
|
|
from app.modules.also.models.schemas import (
|
|
|
|
|
AlsoApproveResult,
|
|
|
|
|
AlsoCompanyMappingUpsert,
|
|
|
|
|
AlsoDifferenceItem,
|
|
|
|
|
AlsoDashboardSummaryResponse,
|
|
|
|
|
AlsoImportJobCreate,
|
|
|
|
|
AlsoImportJobResponse,
|
|
|
|
|
AlsoImportLinesRequest,
|
2026-07-03 20:20:22 +02:00
|
|
|
AlsoManualCompanyMapRequest,
|
|
|
|
|
AlsoManualProductMapRequest,
|
2026-06-20 03:29:51 +02:00
|
|
|
AlsoProductMappingUpsert,
|
|
|
|
|
AlsoQueueApproveRequest,
|
|
|
|
|
AlsoQueueLineResponse,
|
|
|
|
|
AlsoQueueProcessRequest,
|
|
|
|
|
AlsoQueueProcessResult,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _user_id_from_request(request: Request) -> Optional[int]:
|
|
|
|
|
raw_user_id = getattr(request.state, "user_id", None)
|
|
|
|
|
if raw_user_id is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return int(raw_user_id)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/also/config")
|
|
|
|
|
async def also_config() -> dict:
|
|
|
|
|
return also_service.get_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/import-jobs", response_model=AlsoImportJobResponse)
|
|
|
|
|
async def create_import_job(payload: AlsoImportJobCreate, request: Request):
|
|
|
|
|
return also_service.create_import_job(payload, _user_id_from_request(request))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/also/import-jobs", response_model=list[AlsoImportJobResponse])
|
|
|
|
|
async def list_import_jobs(
|
|
|
|
|
status: Optional[str] = Query(default=None),
|
|
|
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
|
|
|
):
|
|
|
|
|
return also_service.list_import_jobs(status=status, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 20:20:22 +02:00
|
|
|
@router.get("/also/import-jobs/{job_id}")
|
2026-06-20 03:29:51 +02:00
|
|
|
async def get_import_job(job_id: int):
|
|
|
|
|
return also_service.get_import_job(job_id)
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 20:20:22 +02:00
|
|
|
@router.delete("/also/import-jobs/{job_id}")
|
|
|
|
|
async def delete_import_job(
|
|
|
|
|
job_id: int,
|
|
|
|
|
delete_order_drafts: bool = Query(default=False),
|
|
|
|
|
):
|
|
|
|
|
return also_service.delete_import_job(job_id=job_id, delete_order_drafts=delete_order_drafts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/import-jobs/{job_id}/auto-map")
|
|
|
|
|
async def auto_map_import_job(job_id: int):
|
|
|
|
|
return also_service.auto_map_import_job(job_id=job_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/import-lines/{line_id}/map-company")
|
|
|
|
|
async def manual_map_company_for_line(
|
|
|
|
|
line_id: int,
|
|
|
|
|
payload: AlsoManualCompanyMapRequest,
|
|
|
|
|
request: Request,
|
|
|
|
|
):
|
|
|
|
|
return also_service.manual_map_company_for_line(
|
|
|
|
|
line_id=line_id,
|
|
|
|
|
customer_id=payload.customer_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
updated_by_user_id=_user_id_from_request(request),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/import-lines/{line_id}/map-product")
|
|
|
|
|
async def manual_map_product_for_line(
|
|
|
|
|
line_id: int,
|
|
|
|
|
payload: AlsoManualProductMapRequest,
|
|
|
|
|
request: Request,
|
|
|
|
|
):
|
|
|
|
|
return also_service.manual_map_product_for_line(
|
|
|
|
|
line_id=line_id,
|
|
|
|
|
product_id=payload.product_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
updated_by_user_id=_user_id_from_request(request),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/import-upload")
|
|
|
|
|
async def upload_import_file(
|
|
|
|
|
request: Request,
|
|
|
|
|
file: UploadFile = File(...),
|
|
|
|
|
):
|
|
|
|
|
payload = await file.read()
|
|
|
|
|
return also_service.import_billing_upload(
|
|
|
|
|
file_name=file.filename or "also-billing.xlsx",
|
|
|
|
|
file_bytes=payload,
|
|
|
|
|
imported_by_user_id=_user_id_from_request(request),
|
|
|
|
|
source_label="Manual upload",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 03:29:51 +02:00
|
|
|
@router.post("/also/import-jobs/{job_id}/lines")
|
|
|
|
|
async def import_lines(job_id: int, payload: AlsoImportLinesRequest):
|
|
|
|
|
return also_service.import_lines(job_id=job_id, payload=payload)
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 20:20:22 +02:00
|
|
|
@router.get("/also/import-jobs/{job_id}/lines")
|
|
|
|
|
async def get_import_job_lines(
|
|
|
|
|
job_id: int,
|
|
|
|
|
status: Optional[str] = Query(default=None),
|
|
|
|
|
limit: int = Query(default=500, ge=1, le=2000),
|
|
|
|
|
):
|
|
|
|
|
return also_service.get_import_job_lines(job_id=job_id, status=status, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 03:29:51 +02:00
|
|
|
@router.get("/also/queue", response_model=list[AlsoQueueLineResponse])
|
|
|
|
|
async def get_queue(
|
|
|
|
|
status: Optional[str] = Query(default=None),
|
|
|
|
|
limit: int = Query(default=200, ge=1, le=1000),
|
|
|
|
|
):
|
|
|
|
|
return also_service.get_queue(status=status, limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/also/dashboard/summary", response_model=AlsoDashboardSummaryResponse)
|
|
|
|
|
async def get_dashboard_summary():
|
|
|
|
|
return also_service.get_dashboard_summary()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/also/dashboard/differences", response_model=list[AlsoDifferenceItem])
|
|
|
|
|
async def get_dashboard_differences(limit: int = Query(default=50, ge=1, le=200)):
|
|
|
|
|
return also_service.get_monthly_differences(limit=limit)
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 20:20:22 +02:00
|
|
|
@router.get("/also/dashboard/status-breakdown")
|
|
|
|
|
async def get_status_breakdown():
|
|
|
|
|
return also_service.get_status_breakdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/also/dashboard/monthly-history")
|
|
|
|
|
async def get_monthly_history(months: int = Query(default=6, ge=1, le=24)):
|
|
|
|
|
return also_service.get_monthly_history(months=months)
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 03:29:51 +02:00
|
|
|
@router.post("/also/queue/run-matching", response_model=AlsoQueueProcessResult)
|
|
|
|
|
async def run_matching(payload: AlsoQueueProcessRequest):
|
|
|
|
|
return also_service.run_matching(
|
|
|
|
|
import_job_id=payload.import_job_id,
|
|
|
|
|
line_ids=payload.line_ids,
|
|
|
|
|
limit=payload.limit,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/queue/run-validation", response_model=AlsoQueueProcessResult)
|
|
|
|
|
async def run_validation(payload: AlsoQueueProcessRequest):
|
|
|
|
|
return also_service.run_validation(
|
|
|
|
|
import_job_id=payload.import_job_id,
|
|
|
|
|
line_ids=payload.line_ids,
|
|
|
|
|
limit=payload.limit,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/also/queue/approve", response_model=AlsoApproveResult)
|
|
|
|
|
async def approve_queue(payload: AlsoQueueApproveRequest, request: Request):
|
|
|
|
|
return also_service.approve_lines_to_drafts(
|
|
|
|
|
import_job_id=payload.import_job_id,
|
|
|
|
|
line_ids=payload.line_ids,
|
|
|
|
|
approved_by_user_id=_user_id_from_request(request),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/also/mappings/company")
|
|
|
|
|
async def upsert_company_mapping(payload: AlsoCompanyMappingUpsert):
|
|
|
|
|
return also_service.upsert_company_mapping(payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/also/mappings/product")
|
|
|
|
|
async def upsert_product_mapping(payload: AlsoProductMappingUpsert):
|
|
|
|
|
return also_service.upsert_product_mapping(payload)
|