first ruggedy draft

This commit is contained in:
2026-06-28 13:48:15 +02:00
parent c916f64fcb
commit 1053f6a2a3
78 changed files with 20905 additions and 2 deletions

View File

@@ -0,0 +1,782 @@
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