"""Local Postgres mirror of the YNAB budget. Rows are keyed by YNAB's own ids and kept current by ``blueprints.ynab.sync`` using YNAB delta requests. Money is stored in dollars with 3 decimal places, which is exactly YNAB's milliunits. Deleted YNAB entities are kept with ``deleted=True`` rather than removed. """ from tortoise import fields from tortoise.models import Model def money(**kwargs): return fields.DecimalField(max_digits=15, decimal_places=3, **kwargs) class YnabAccount(Model): id = fields.CharField(max_length=36, primary_key=True) name = fields.CharField(max_length=255) type = fields.CharField(max_length=50) on_budget = fields.BooleanField() closed = fields.BooleanField() note = fields.TextField(null=True) balance = money() cleared_balance = money() uncleared_balance = money() last_reconciled_at = fields.DatetimeField(null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_accounts" class YnabPayee(Model): id = fields.CharField(max_length=36, primary_key=True) name = fields.CharField(max_length=500) transfer_account_id = fields.CharField(max_length=36, null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_payees" class YnabCategoryGroup(Model): id = fields.CharField(max_length=36, primary_key=True) name = fields.CharField(max_length=255) hidden = fields.BooleanField() internal = fields.BooleanField(default=False) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_category_groups" class YnabCategory(Model): id = fields.CharField(max_length=36, primary_key=True) category_group_id = fields.CharField(max_length=36, db_index=True) name = fields.CharField(max_length=255) hidden = fields.BooleanField() internal = fields.BooleanField(default=False) note = fields.TextField(null=True) goal_type = fields.CharField(max_length=20, null=True) goal_target = money(null=True) goal_target_month = fields.DateField(null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_categories" class YnabMonth(Model): # ISO month start, e.g. "2026-10-01" id = fields.CharField(max_length=10, primary_key=True) month = fields.DateField(db_index=True) note = fields.TextField(null=True) income = money() budgeted = money() activity = money() to_be_budgeted = money() age_of_money = fields.IntField(null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_months" class YnabMonthCategory(Model): """Per-month category amounts. ``budgeted`` is what YNAB calls "Assigned".""" # ":" id = fields.CharField(max_length=47, primary_key=True) month = fields.DateField(db_index=True) category_id = fields.CharField(max_length=36, db_index=True) budgeted = money() activity = money() balance = money() deleted = fields.BooleanField(default=False) class Meta: table = "ynab_month_categories" class YnabTransaction(Model): id = fields.CharField(max_length=64, primary_key=True) date = fields.DateField(db_index=True) amount = money() memo = fields.TextField(null=True) cleared = fields.CharField(max_length=20) approved = fields.BooleanField() flag_color = fields.CharField(max_length=20, null=True) account_id = fields.CharField(max_length=36, db_index=True) payee_id = fields.CharField(max_length=36, null=True, db_index=True) category_id = fields.CharField(max_length=36, null=True, db_index=True) transfer_account_id = fields.CharField(max_length=36, null=True) import_payee_name = fields.CharField(max_length=500, null=True) # Denormalized from payees/categories/accounts after each sync for easy querying account_name = fields.CharField(max_length=255, null=True) payee_name = fields.CharField(max_length=500, null=True) category_name = fields.CharField(max_length=255, null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_transactions" class YnabSubtransaction(Model): """Lines of a split transaction; the parent's category is "Split".""" id = fields.CharField(max_length=64, primary_key=True) transaction_id = fields.CharField(max_length=64, db_index=True) amount = money() memo = fields.TextField(null=True) payee_id = fields.CharField(max_length=36, null=True) category_id = fields.CharField(max_length=36, null=True, db_index=True) transfer_account_id = fields.CharField(max_length=36, null=True) deleted = fields.BooleanField(default=False) class Meta: table = "ynab_subtransactions" class YnabChange(Model): """Append-only log of what each sync changed, built by diffing against the mirror. YNAB's API has no history, so this is the only record of edits (e.g. an assignment going from $400 to $500). It starts at the first sync. """ id = fields.BigIntField(primary_key=True) synced_at = fields.DatetimeField(auto_now_add=True, db_index=True) server_knowledge = fields.BigIntField() entity_type = fields.CharField(max_length=30, db_index=True) entity_id = fields.CharField(max_length=64, db_index=True) # created / updated / deleted action = fields.CharField(max_length=10) label = fields.CharField(max_length=500) # {field: [old, new]}; for "created", old is null changes = fields.JSONField() class Meta: table = "ynab_changes" class YnabSyncState(Model): budget_id = fields.CharField(max_length=64, primary_key=True) budget_name = fields.CharField(max_length=255, null=True) server_knowledge = fields.BigIntField(null=True) last_synced_at = fields.DatetimeField(null=True) last_full_sync_at = fields.DatetimeField(null=True) last_error = fields.TextField(null=True) class Meta: table = "ynab_sync_state"