# Copyright (c) 2025 Patrick Motsch # All rights reserved. """Background job models: generic, reusable infrastructure for long-running tasks. A `BackgroundJob` record tracks the lifecycle of one async task that must not block the calling HTTP request. Any caller (HTTP route, AI tool, scheduled task) can: 1. Register a handler once via `registerJobHandler(jobType, handler)`. 2. Submit work via `startJob(jobType, payload, ...)` which returns a `jobId` immediately and runs the handler in the background. 3. Poll `getJobStatus(jobId)` (HTTP `GET /api/jobs/{jobId}`) until `status` is one of {SUCCESS, ERROR, CANCELLED}. See `modules.serviceCenter.services.serviceBackgroundJobs.mainBackgroundJobService`. """ from typing import Any, Dict, Optional from enum import Enum from datetime import datetime, timezone import uuid from pydantic import Field from modules.datamodels.datamodelBase import PowerOnModel from modules.shared.i18nRegistry import i18nModel class BackgroundJobStatusEnum(str, Enum): """Lifecycle status of a background job.""" PENDING = "PENDING" RUNNING = "RUNNING" SUCCESS = "SUCCESS" ERROR = "ERROR" CANCELLED = "CANCELLED" TERMINAL_JOB_STATUSES = { BackgroundJobStatusEnum.SUCCESS, BackgroundJobStatusEnum.ERROR, BackgroundJobStatusEnum.CANCELLED, } @i18nModel("Hintergrund-Job") class BackgroundJob(PowerOnModel): """Generic record describing a long-running asynchronous task. Scope: the combination of `mandateId` and optionally `featureInstanceId` is used for access control on `GET /api/jobs/{jobId}`. """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"label": "ID"}, ) jobType: str = Field( ..., description="Handler key registered via registerJobHandler() (e.g. 'trusteeAccountingSync')", json_schema_extra={"label": "Typ"}, ) mandateId: Optional[str] = Field( None, description="Mandate scope (used for access checks). None for system-wide jobs.", json_schema_extra={ "label": "Mandanten-ID", "fk_target": {"db": "poweron_app", "table": "Mandate"}, }, ) featureInstanceId: Optional[str] = Field( None, description="Feature instance scope (optional)", json_schema_extra={ "label": "Feature-Instanz", "fk_target": {"db": "poweron_app", "table": "FeatureInstance"}, }, ) triggeredBy: Optional[str] = Field( None, description="UserId or 'ai-tool:' / 'scheduler:'", json_schema_extra={"label": "Ausgeloest von"}, ) status: str = Field( default=BackgroundJobStatusEnum.PENDING.value, description="Current lifecycle status", json_schema_extra={"label": "Status"}, ) progress: int = Field( default=0, description="Progress 0..100 (best-effort; may stay 0 for handlers that cannot estimate)", json_schema_extra={"label": "Fortschritt"}, ) progressMessage: Optional[str] = Field( None, description="Human-readable current step (e.g. 'Importing journal entries...')", json_schema_extra={"label": "Fortschritts-Nachricht"}, ) payload: Dict[str, Any] = Field( default_factory=dict, description="Job input parameters (JSON)", json_schema_extra={"label": "Eingabe"}, ) result: Optional[Dict[str, Any]] = Field( None, description="Handler return value on success (JSON)", json_schema_extra={"label": "Ergebnis"}, ) errorMessage: Optional[str] = Field( None, description="Truncated error message on failure (full stack trace in logs)", json_schema_extra={"label": "Fehler"}, ) createdAt: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), description="When the job was submitted", json_schema_extra={"label": "Eingereicht"}, ) startedAt: Optional[datetime] = Field( None, description="When the handler began running", json_schema_extra={"label": "Gestartet"}, ) finishedAt: Optional[datetime] = Field( None, description="When the handler reached a terminal status", json_schema_extra={"label": "Beendet"}, )