from __future__ import annotations from datetime import date, datetime from decimal import Decimal from typing import Any from mousehold_domain import ( AccountType, EventObservationRelation, FinancialEventStatus, FinancialEventType, ImportConnectionStatus, ImportConnectionType, ObservationStatus, PaymentLineStatus, PaymentStatus, PlanningStatus, ProjectStatus, ReconciliationStatus, RecurringCommitmentStatus, RecurringInterval, RelationType, SettlementStatus, SourceType, VisibilityMode, ) from pydantic import BaseModel, ConfigDict, Field class ApiModel(BaseModel): model_config = ConfigDict(from_attributes=True) class HouseholdCreate(ApiModel): name: str default_currency: str = "EUR" class HouseholdUpdate(ApiModel): name: str | None = None default_currency: str | None = None class HouseholdRead(ApiModel): id: str name: str default_currency: str created_at: datetime class UserCreate(ApiModel): household_id: str display_name: str email: str | None = None class UserUpdate(ApiModel): display_name: str | None = None email: str | None = None class UserRead(ApiModel): id: str household_id: str display_name: str email: str | None created_at: datetime class LoginIdentityCreate(ApiModel): user_id: str provider: str provider_subject: str email: str | None = None display_name: str | None = None class LoginIdentityRead(ApiModel): id: str user_id: str provider: str provider_subject: str email: str | None display_name: str | None created_at: datetime class LocalLoginRequest(ApiModel): user_id: str class LocalSessionRead(ApiModel): id: str user_id: str household_id: str created_at: datetime last_seen_at: datetime expires_at: datetime class AuthSessionRead(ApiModel): session: LocalSessionRead user: UserRead household: HouseholdRead class AccountCreate(ApiModel): household_id: str owner_user_id: str | None = None name: str account_type: AccountType institution_name: str | None = None currency: str = "EUR" visibility_mode: VisibilityMode = VisibilityMode.PRIVATE is_active: bool = True class AccountUpdate(ApiModel): owner_user_id: str | None = None name: str | None = None account_type: AccountType | None = None institution_name: str | None = None currency: str | None = None visibility_mode: VisibilityMode | None = None is_active: bool | None = None class AccountRead(ApiModel): id: str household_id: str owner_user_id: str | None name: str account_type: AccountType institution_name: str | None currency: str visibility_mode: VisibilityMode is_active: bool created_at: datetime class SplitLineCreate(ApiModel): user_id: str share_amount: Decimal share_percent: Decimal | None = None reason: str = "manual" class SplitLineRead(ApiModel): id: str event_id: str user_id: str share_amount: Decimal share_percent: Decimal | None reason: str created_at: datetime class ManualObservationCreate(ApiModel): household_id: str source_type: SourceType = SourceType.MANUAL source_account_id: str | None = None owner_user_id: str | None = None observed_at: datetime | None = None source_reference: str | None = None provider_transaction_id: str | None = None message_id: str | None = None raw_text: str | None = None raw_payload_json: dict[str, Any] | None = None amount: Decimal currency: str = "EUR" event_date: date booking_date: date | None = None due_date: date | None = None merchant: str | None = None counterparty: str | None = None description: str | None = None event_type: FinancialEventType = FinancialEventType.EXPENSE status: FinancialEventStatus | None = None payer_user_id: str | None = None payment_account_id: str | None = None category_id: str | None = None project_id: str | None = None split_lines: list[SplitLineCreate] = Field(default_factory=list) class ObservationImportCreate(ManualObservationCreate): source_type: SourceType class RawObservationRead(ApiModel): id: str household_id: str source_type: SourceType source_account_id: str | None owner_user_id: str | None observed_at: datetime | None received_at: datetime raw_hash: str source_reference: str | None provider_transaction_id: str | None message_id: str | None raw_text: str | None raw_file_path: str | None raw_payload_json: dict[str, Any] | None parsed_json: dict[str, Any] | None parser_name: str | None parser_version: str | None confidence: Decimal status: ObservationStatus created_at: datetime class EventObservationLinkRead(ApiModel): id: str event_id: str observation_id: str relation: EventObservationRelation confidence: Decimal created_at: datetime class EventCreate(ApiModel): household_id: str source_account_id: str | None = None payment_account_id: str | None = None payer_user_id: str | None = None event_type: FinancialEventType = FinancialEventType.EXPENSE status: FinancialEventStatus = FinancialEventStatus.CANDIDATE event_date: date booking_date: date | None = None due_date: date | None = None amount: Decimal currency: str = "EUR" merchant: str | None = None counterparty: str | None = None description: str | None = None category_id: str | None = None project_id: str | None = None created_by_user_id: str | None = None split_lines: list[SplitLineCreate] = Field(default_factory=list) class EventUpdate(ApiModel): source_account_id: str | None = None payment_account_id: str | None = None payer_user_id: str | None = None event_type: FinancialEventType | None = None status: FinancialEventStatus | None = None event_date: date | None = None booking_date: date | None = None due_date: date | None = None amount: Decimal | None = None currency: str | None = None merchant: str | None = None counterparty: str | None = None description: str | None = None category_id: str | None = None project_id: str | None = None class EventRead(ApiModel): id: str household_id: str source_account_id: str | None payment_account_id: str | None payer_user_id: str | None event_type: FinancialEventType status: FinancialEventStatus event_date: date booking_date: date | None due_date: date | None amount: Decimal currency: str merchant: str | None counterparty: str | None description: str | None category_id: str | None project_id: str | None created_by_user_id: str | None fingerprint: str | None created_at: datetime updated_at: datetime split_lines: list[SplitLineRead] = Field(default_factory=list) tag_ids: list[str] = Field(default_factory=list) class EventRelationCreate(ApiModel): to_event_id: str relation_type: RelationType confidence: Decimal = Decimal("1.00") created_by: str = "user" class EventRelationRead(ApiModel): id: str from_event_id: str to_event_id: str relation_type: RelationType confidence: Decimal created_by: str created_at: datetime class CategoryCreate(ApiModel): household_id: str name: str parent_id: str | None = None path: str | None = None is_active: bool = True class CategoryUpdate(ApiModel): name: str | None = None parent_id: str | None = None path: str | None = None is_active: bool | None = None class CategoryRead(ApiModel): id: str household_id: str parent_id: str | None name: str path: str is_active: bool class TagCreate(ApiModel): household_id: str name: str class TagUpdate(ApiModel): name: str | None = None class TagRead(ApiModel): id: str household_id: str name: str class EventTagsUpdate(ApiModel): tag_ids: list[str] = Field(default_factory=list) class EventDetailRead(ApiModel): event: EventRead category: CategoryRead | None = None tags: list[TagRead] = Field(default_factory=list) observation_links: list[EventObservationLinkRead] = Field(default_factory=list) observations: list[RawObservationRead] = Field(default_factory=list) relations: list[EventRelationRead] = Field(default_factory=list) related_events: list[EventRead] = Field(default_factory=list) class ObservationReviewItem(ApiModel): observation: RawObservationRead events: list[EventRead] = Field(default_factory=list) action_required: bool = True suggested_action: str = "review" class ObservationAcceptRequest(ApiModel): event_status: FinancialEventStatus | None = None create_event_if_missing: bool = True class ObservationUpdateRequest(ApiModel): source_account_id: str | None = None owner_user_id: str | None = None raw_text: str | None = None amount: Decimal | None = None currency: str | None = None event_date: date | None = None booking_date: date | None = None due_date: date | None = None merchant: str | None = None counterparty: str | None = None description: str | None = None event_type: FinancialEventType | None = None status: FinancialEventStatus | None = None sync_linked_events: bool = True class ObservationIgnoreRequest(ApiModel): ignore_linked_events: bool = False reason: str | None = None class ObservationLinkRequest(ApiModel): event_id: str relation: EventObservationRelation = EventObservationRelation.EVIDENCE_FOR class ObservationBatchRequest(ApiModel): observation_ids: list[str] action: str event_status: FinancialEventStatus | None = None ignore_linked_events: bool = False class ObservationBatchResponse(ApiModel): updated_count: int items: list[ObservationReviewItem] class SettlementTransfer(ApiModel): from_user_id: str to_user_id: str amount: Decimal currency: str class SettlementUserBalance(ApiModel): user_id: str paid: Decimal share: Decimal net: Decimal class SettlementSummary(ApiModel): household_id: str project_id: str | None currency: str included_event_ids: list[str] excluded_event_ids: list[str] balances: list[SettlementUserBalance] suggested_transfers: list[SettlementTransfer] explanation: list[str] class ProjectCreate(ApiModel): household_id: str name: str description: str | None = None start_date: date | None = None end_date: date | None = None default_split_rule_id: str | None = None status: ProjectStatus = ProjectStatus.PLANNING currency: str = "EUR" participant_user_ids: list[str] = Field(default_factory=list) class ProjectRead(ApiModel): id: str household_id: str name: str description: str | None start_date: date | None end_date: date | None default_split_rule_id: str | None status: ProjectStatus currency: str created_at: datetime updated_at: datetime class ProjectParticipantCreate(ApiModel): user_id: str role: str = "participant" default_share_percent: Decimal | None = None class ProjectParticipantRead(ApiModel): id: str project_id: str user_id: str role: str default_share_percent: Decimal | None class ProjectBudgetLineCreate(ApiModel): category_id: str | None = None name: str budgeted_amount: Decimal = Decimal("0.00") sort_order: int = 0 notes: str | None = None class ProjectBudgetLineRead(ApiModel): id: str project_id: str category_id: str | None name: str budgeted_amount: Decimal committed_amount_cached: Decimal | None paid_amount_cached: Decimal | None sort_order: int notes: str | None class ProjectPlannedItemCreate(ApiModel): budget_line_id: str | None = None description: str estimated_amount: Decimal = Decimal("0.00") committed_amount: Decimal | None = None expected_date: date | None = None due_date: date | None = None expected_payer_account_id: str | None = None split_rule_id: str | None = None planning_status: PlanningStatus = PlanningStatus.IDEA payment_status: PaymentStatus = PaymentStatus.UNPAID reconciliation_status: ReconciliationStatus = ReconciliationStatus.NO_EVIDENCE settlement_status: SettlementStatus = SettlementStatus.UNSETTLED notes: str | None = None class ProjectPlannedItemRead(ApiModel): id: str project_id: str budget_line_id: str | None description: str estimated_amount: Decimal committed_amount: Decimal | None expected_date: date | None due_date: date | None expected_payer_account_id: str | None split_rule_id: str | None planning_status: PlanningStatus payment_status: PaymentStatus reconciliation_status: ReconciliationStatus settlement_status: SettlementStatus notes: str | None created_at: datetime updated_at: datetime class PaymentLineCreate(ApiModel): planned_item_id: str | None = None event_id: str | None = None amount: Decimal currency: str = "EUR" date: date paid_by_user_id: str | None = None payment_account_id: str | None = None source_observation_id: str | None = None status: PaymentLineStatus = PaymentLineStatus.EXPECTED class PaymentLineRead(ApiModel): id: str planned_item_id: str | None event_id: str | None amount: Decimal currency: str date: date paid_by_user_id: str | None payment_account_id: str | None source_observation_id: str | None status: PaymentLineStatus created_at: datetime class ProjectSummary(ApiModel): project_id: str currency: str budgeted_total: Decimal committed_total: Decimal paid_total: Decimal reconciled_total: Decimal settled_total: Decimal due_later_total: Decimal remaining_uncommitted_budget: Decimal open_actions: list[str] class ImportConnectionCreate(ApiModel): household_id: str owner_user_id: str connection_type: ImportConnectionType display_name: str status: ImportConnectionStatus = ImportConnectionStatus.NEEDS_AUTH secret_storage_ref: str | None = None runs_locally: bool = True connector_config_json: dict[str, Any] | None = None class ImportConnectionUpdate(ApiModel): owner_user_id: str | None = None connection_type: ImportConnectionType | None = None display_name: str | None = None status: ImportConnectionStatus | None = None secret_storage_ref: str | None = None runs_locally: bool | None = None connector_config_json: dict[str, Any] | None = None class ImportConnectionRead(ApiModel): id: str household_id: str owner_user_id: str connection_type: ImportConnectionType display_name: str status: ImportConnectionStatus last_sync_at: datetime | None last_error: str | None secret_storage_ref: str | None runs_locally: bool connector_config_json: dict[str, Any] | None created_at: datetime updated_at: datetime class ImportRunRead(ApiModel): id: str household_id: str connection_id: str | None actor_user_id: str | None source_type: str | None status: str imported_count: int duplicate_count: int created_event_count: int skipped_count: int error: str | None summary_json: dict[str, Any] | None started_at: datetime finished_at: datetime | None class ImportRunDetailRead(ApiModel): run: ImportRunRead observations: list[RawObservationRead] = Field(default_factory=list) events: list[EventRead] = Field(default_factory=list) class LocalAgentRegister(ApiModel): household_id: str owner_user_id: str display_name: str = "Local connector agent" class LocalAgentHeartbeat(ApiModel): connection_id: str status: ImportConnectionStatus = ImportConnectionStatus.ACTIVE last_error: str | None = None class SyncObservationsRequest(ApiModel): connection_id: str observations: list[ObservationImportCreate] class SyncObservationsResponse(ApiModel): imported_observation_ids: list[str] created_event_ids: list[str] duplicate_observation_ids: list[str] connection_id: str | None = None imported_count: int = 0 duplicate_count: int = 0 created_event_count: int = 0 run: ImportRunRead | None = None class BackupResult(ApiModel): file_path: str size_bytes: int created_at: datetime class HouseholdExport(ApiModel): household: dict[str, Any] exported_at: datetime counts: dict[str, int] data: dict[str, list[dict[str, Any]]] class RecurringCommitmentCreate(ApiModel): household_id: str provider: str description: str | None = None amount_expected: Decimal amount_min: Decimal | None = None amount_max: Decimal | None = None currency: str = "EUR" interval: RecurringInterval next_due_date: date payer_account_id: str | None = None category_id: str | None = None project_id: str | None = None notice_period: str | None = None renewal_date: date | None = None status: RecurringCommitmentStatus = RecurringCommitmentStatus.ACTIVE matching_rule_json: dict[str, Any] | None = None class RecurringCommitmentUpdate(ApiModel): provider: str | None = None description: str | None = None amount_expected: Decimal | None = None amount_min: Decimal | None = None amount_max: Decimal | None = None currency: str | None = None interval: RecurringInterval | None = None next_due_date: date | None = None payer_account_id: str | None = None category_id: str | None = None project_id: str | None = None notice_period: str | None = None renewal_date: date | None = None status: RecurringCommitmentStatus | None = None matching_rule_json: dict[str, Any] | None = None class RecurringCommitmentRead(ApiModel): id: str household_id: str provider: str description: str | None amount_expected: Decimal amount_min: Decimal | None amount_max: Decimal | None currency: str interval: RecurringInterval next_due_date: date payer_account_id: str | None category_id: str | None project_id: str | None notice_period: str | None renewal_date: date | None status: RecurringCommitmentStatus matching_rule_json: dict[str, Any] | None created_at: datetime updated_at: datetime class ReconciliationEventSummary(ApiModel): id: str source_type: str | None event_type: str status: str event_date: date amount: Decimal currency: str merchant: str | None counterparty: str | None description: str | None source_reference: str | None provider_transaction_id: str | None observation_id: str | None class ReconciliationSuggestionRead(ApiModel): id: str kind: str relation_type: RelationType primary_event: ReconciliationEventSummary confirming_event: ReconciliationEventSummary confidence: Decimal reasons: list[str] action: str class ReconciliationAcceptResponse(ApiModel): accepted: ReconciliationSuggestionRead class ReconciliationIgnoreRequest(ApiModel): actor_user_id: str | None = None reason: str | None = None class ReconciliationDecisionRead(ApiModel): id: str household_id: str suggestion_id: str decision: str kind: str relation_type: RelationType primary_event_id: str confirming_event_id: str confidence: Decimal actor_user_id: str | None reason: str | None created_at: datetime updated_at: datetime class ReconciliationIgnoreResponse(ApiModel): ignored: ReconciliationSuggestionRead decision: ReconciliationDecisionRead