We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jcvalerio/moneywiz-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Transaction data models for MoneyWiz MCP Server."""
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Any
class TransactionType(Enum):
"""Transaction types based on MoneyWiz Core Data entities."""
DEPOSIT = "deposit" # ENT 37 - DepositTransaction
WITHDRAW = "withdraw" # ENT 47 - WithdrawTransaction
TRANSFER_IN = "transfer_in" # ENT 45 - TransferDepositTransaction
TRANSFER_OUT = "transfer_out" # ENT 46 - TransferWithdrawTransaction
INVESTMENT_BUY = "investment_buy" # ENT 40
INVESTMENT_SELL = "investment_sell" # ENT 41
INVESTMENT_EXCHANGE = "investment_exchange" # ENT 38
REFUND = "refund" # ENT 43
RECONCILE = "reconcile" # ENT 42
TRANSFER_BUDGET = "transfer_budget" # ENT 44
ADJUST_BALANCE = "adjust_balance" # Balance adjustment transactions
UNKNOWN = "unknown"
@dataclass
class DateRange:
"""Date range for filtering transactions."""
start_date: datetime
end_date: datetime
def __post_init__(self) -> None:
if self.start_date > self.end_date:
raise ValueError("Start date must be before end date")
@dataclass
class TransactionModel:
"""
Unified transaction model based on MoneyWiz Core Data structure.
Based on analysis of moneywiz-api/src/moneywiz_api/model/transaction.py:
- All transaction entities use ZAMOUNT1 for the account-level amount
- ZACCOUNT2 links transactions to accounts
- ZDATE1 contains the transaction date
- ZDESC2 contains the description
"""
# Core transaction data from ZSYNCOBJECT
id: str # Z_PK as string
entity_id: int # Z_ENT (37, 45, 46, 47, etc.)
account_id: int # ZACCOUNT2
# Transaction details
amount: Decimal # ZAMOUNT1 - account-level amount
date: datetime # ZDATE1
description: str # ZDESC2
notes: str | None # ZNOTES1
reconciled: bool # ZRECONCILED
# Classification
transaction_type: TransactionType
category: str | None # Category name (from entity mapping)
category_id: int | None # Category Z_PK
parent_category: str | None # Parent category name
parent_category_id: int | None # Parent category Z_PK
category_path: str | None # Full hierarchy "Food & Dining ▶ Groceries"
category_hierarchy: list[str] # ["Food & Dining", "Groceries"]
payee: str | None # Payee name (from entity mapping)
payee_id: int | None # ZPAYEE2
# Currency information
currency: str # Account currency
original_currency: str | None # ZORIGINALCURRENCY
original_amount: Decimal | None # ZORIGINALAMOUNT
exchange_rate: Decimal | None # ZORIGINALEXCHANGERATE
# Transfer-specific fields
related_account_id: int | None = None # For transfers
related_transaction_id: int | None = None # For transfers
# Investment-specific fields
investment_holding_id: int | None = None
number_of_shares: Decimal | None = None
price_per_share: Decimal | None = None
fee: Decimal | None = None
# Tags (populated by service enhancement)
tags: list[str] = field(default_factory=list) # Tag names from Z_36TAGS
@classmethod
def from_raw_data(cls, row: dict[str, Any]) -> "TransactionModel":
"""
Create TransactionModel from raw Core Data row.
Args:
row: Raw ZSYNCOBJECT row data
Returns:
TransactionModel instance
"""
entity_id = row.get("Z_ENT", 0)
# Map entity to transaction type
entity_type_map = {
37: TransactionType.DEPOSIT,
45: TransactionType.TRANSFER_IN,
46: TransactionType.TRANSFER_OUT,
47: TransactionType.WITHDRAW,
40: TransactionType.INVESTMENT_BUY,
41: TransactionType.INVESTMENT_SELL,
38: TransactionType.INVESTMENT_EXCHANGE,
43: TransactionType.REFUND,
42: TransactionType.RECONCILE,
44: TransactionType.TRANSFER_BUDGET,
}
transaction_type = entity_type_map.get(entity_id, TransactionType.UNKNOWN)
# Convert date (Core Data timestamp to Python datetime)
date_timestamp = row.get("ZDATE1", 0)
if date_timestamp:
# Core Data uses NSDate which is seconds since 2001-01-01
base_date = datetime(2001, 1, 1)
date = base_date.timestamp() + date_timestamp
transaction_date = datetime.fromtimestamp(date)
else:
transaction_date = datetime.now()
# Convert amounts to Decimal for precision
amount = Decimal(str(row.get("ZAMOUNT1", 0)))
original_amount = None
if row.get("ZORIGINALAMOUNT"):
original_amount = Decimal(str(row.get("ZORIGINALAMOUNT", 0)))
exchange_rate = None
if row.get("ZORIGINALEXCHANGERATE"):
exchange_rate = Decimal(str(row.get("ZORIGINALEXCHANGERATE", 0)))
return cls(
id=str(row.get("Z_PK", "")),
entity_id=entity_id,
account_id=row.get("ZACCOUNT2", 0),
amount=amount,
date=transaction_date,
description=row.get("ZDESC2", ""),
notes=row.get("ZNOTES1"),
reconciled=bool(row.get("ZRECONCILED", 0)),
transaction_type=transaction_type,
category=None, # Will be resolved via ZCATEGORYASSIGMENT
category_id=row.get(
"ZCATEGORY2"
), # Direct category reference or via ZCATEGORYASSIGMENT
parent_category=None, # Will be resolved with hierarchy
parent_category_id=None, # Will be resolved with hierarchy
category_path=None, # Will be built from hierarchy
category_hierarchy=[], # Will be built from hierarchy
payee=None, # Will be resolved separately
payee_id=row.get("ZPAYEE2"),
currency="USD", # Will be resolved from account
original_currency=row.get("ZORIGINALCURRENCY"),
original_amount=original_amount,
exchange_rate=exchange_rate,
related_account_id=(
row.get("ZSENDERACCOUNT") or row.get("ZRECIPIENTACCOUNT1")
),
related_transaction_id=(
row.get("ZSENDERTRANSACTION") or row.get("ZRECIPIENTTRANSACTION")
),
investment_holding_id=row.get("ZINVESTMENTHOLDING"),
number_of_shares=(
Decimal(str(row.get("ZNUMBEROFSHARES1", 0)))
if row.get("ZNUMBEROFSHARES1")
else None
),
price_per_share=(
Decimal(str(row.get("ZPRICEPERSHARE1", 0)))
if row.get("ZPRICEPERSHARE1")
else None
),
fee=(Decimal(str(row.get("ZFEE2", 0))) if row.get("ZFEE2") else None),
)
def is_expense(self) -> bool:
"""Check if transaction is an expense (negative amount)."""
return self.amount < 0
def is_income(self) -> bool:
"""Check if transaction is income (positive amount)."""
return self.amount > 0
def is_transfer(self) -> bool:
"""Check if transaction is a transfer between accounts."""
return self.transaction_type in [
TransactionType.TRANSFER_IN,
TransactionType.TRANSFER_OUT,
]