テーブル定義書¶
関連: 月次予算 / 支出の保存 / 収入 / PostgreSQL
正は sql/ のマイグレーションである。
Bot 起動時に src/finance_manager/db.py の SCHEMA_FILES 順で適用する。
金額の初期値はスキーマとは別で、sql/budget_seed.example.sql を手動実行する。
| 項目 | 値 |
|---|---|
| DB 名 | finance_manager |
| 書込ユーザー | finance_app(Bot) |
| 読取ユーザー | finance_readonly(Grafana) |
| 分類の正 | テーブルではなく src/finance_manager/taxonomy.py |
全体像¶
incomes (給与) ──► budget_months.ceiling_yen
budget_settings / category_budgets ──► 初回 draft のフォールバック
fixed_cost_items ──► budget_months.fixed_costs_yen / variable_pool_yen
budget_month_categories ──► 変動カテゴリ枠
expenses ──► 変動消化(固定費カテゴリは除外)
surplus_ledger ◄── 未割当 / 月末実残
expenses ◄── notification_raw.expense_id(任意)
wishlist_items / planned_expenses … 固定貯金、別枠とは別
外部キーとして存在する線は、月ヘッダと変動枠、通知と支出の2本だけである。 給与、固定費マスタ、seed、別枠はアプリが読んで計算する。
物理 ER¶
erDiagram
budget_months ||--|{ budget_month_categories : "year_month"
expenses ||--o{ notification_raw : "expense_id"
expenses {
bigint id PK
int amount_yen
text category
timestamptz paid_at
}
incomes {
bigint id PK
int amount_yen
text category
timestamptz received_at
}
budget_months {
date year_month PK
int ceiling_yen
int savings_yen
int fixed_costs_yen
int variable_pool_yen
text status
}
budget_month_categories {
date year_month PK
text category PK
int monthly_limit_yen
}
fixed_cost_items {
bigint id PK
text name UK
int monthly_yen
text expense_category
boolean active
}
surplus_ledger {
bigint id PK
date year_month
int amount_yen
text reason
}
budget_settings {
int id PK
int monthly_income_yen
int monthly_savings_yen
}
category_budgets {
text category PK
int monthly_limit_yen
}
notification_raw {
bigint id PK
bigint expense_id FK
}
wishlist_items {
bigint id PK
text name
int target_yen
}
planned_expenses {
bigint id PK
text name
int target_yen
date deadline
}
wishlist_items と planned_expenses は他テーブルとつながっていない。
固定貯金、別枠貯金とも別の袋である。
予算の流れ¶
草案を作るとき、本来の値(前月給与、前月の変動枠)が無ければ seed を使う。 点線がフォールバックである。
flowchart LR
subgraph 本来の正
I["incomes 前月の給与"] --> BM["budget_months 今月"]
FC["fixed_cost_items"] --> BM
BMC["budget_month_categories"] --> BM
end
subgraph フォールバック
BS["budget_settings"] -.->|給与が無いとき天井| BM
CB["category_budgets"] -.->|初月だけ枠のひな型| BMC
end
E["expenses 変動カテゴリ"] --> SP["変動の消化"]
BM --> U["未割当 / 月末実残"]
U --> SL["surplus_ledger 別枠"]
適用順(SCHEMA_FILES)¶
| ファイル | 内容 |
|---|---|
001〜004 |
expenses 本体、場面、内訳、小分類 |
005 |
budget_settings / category_budgets(フォールバック) |
006 |
incomes |
007 |
budget_months / budget_month_categories |
008 |
wishlist_items / planned_expenses |
009 |
notification_raw |
010 |
旧大分類の読み替え(娯楽、衣服、交通 → 趣味 など) |
011 |
fixed_cost_items / surplus_ledger / 月ヘッダ拡張 |
sql/grafana_readonly_grants.sql は Grafana 用 GRANT の手作業ファイルで、起動時には走らない。
expenses(支出)¶
1件の支出を置く。 Discord 手動が本線である。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | 支出 ID |
source |
TEXT | NOT NULL, default discord |
取込元 |
discord_user_id |
TEXT | 投稿者 | |
amount_yen |
INTEGER | NOT NULL, > 0 |
金額(円) |
merchant |
TEXT | 店名など | |
category |
TEXT | 大分類(taxonomy.py) |
|
memo |
TEXT | メモ | |
paid_at |
TIMESTAMPTZ | 利用日時(UTC で保存)。不明なら null(集計は created_at にフォールバック)。Gemini が返す日時はオフセットの有無に関わらず 日本時間の壁時計 として解釈する(Z / +00:00 付きでも時計の数字を JST にする)。今日/月の集計は (paid_at AT TIME ZONE 'Asia/Tokyo') と JST 壁時計の naive 境界を比較する(UTC に直した境界を渡さない)。境界は db.jst_day_window / db.jst_range_window / db.month_bounds だけで組む |
|
raw_text |
TEXT | NOT NULL | 元メッセージ |
occasion |
TEXT | 場面(朝食 / 夕食 / 遊び など) | |
occasion_reason |
TEXT | 場面推定の短い根拠 | |
line_items |
JSONB | レシート内訳。読めなければ null | |
subcategories |
TEXT[] | NOT NULL, default {} |
小分類(最大5目安) |
created_at |
TIMESTAMPTZ | NOT NULL, now() | 記録日時 |
インデックス: paid_at, created_at, subcategories(GIN)
incomes(収入)¶
支出とは別テーブルである。
ID も別採番し、返事では income_id= と出す。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | 収入 ID |
source |
TEXT | NOT NULL, default discord |
取込元 |
discord_user_id |
TEXT | 投稿者 | |
amount_yen |
INTEGER | NOT NULL, > 0 |
金額 |
category |
TEXT | 給与 / ボーナス / 臨時 / 立替精算 / 給付 / その他 | |
counterparty |
TEXT | 支払元 | |
memo |
TEXT | メモ | |
received_at |
TIMESTAMPTZ | 入金日時。不明なら created_at |
|
raw_text |
TEXT | NOT NULL | 元メッセージ |
created_at |
TIMESTAMPTZ | NOT NULL, now() | 記録日時 |
予算上限に使うのは category = '給与' の前月合計だけである。
ボーナスは記録するが、上限には入れない。
budget_settings / category_budgets(フォールバック)¶
行はほぼ seed 用である。
月次の正は budget_months 側にある。
budget_settings¶
1行だけ置く(id = 1)。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
INTEGER | PK, = 1 |
固定 |
monthly_income_yen |
INTEGER | NOT NULL, > 0 |
前月給与が無いときの天井フォールバック |
monthly_savings_yen |
INTEGER | NOT NULL, default 50000, >= 0 |
固定貯金の初期値 |
updated_at |
TIMESTAMPTZ | NOT NULL |
category_budgets¶
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
category |
TEXT | PK | 変動大分類 |
monthly_limit_yen |
INTEGER | NOT NULL, >= 0 |
月次枠 |
note |
TEXT |
当月行が無いとき、ここから budget_month_categories へコピーする。
その他 と固定費カテゴリは置かない。
budget_months(月ヘッダ)¶
year_month はその月の1日である。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
year_month |
DATE | PK | 対象月(1日) |
ceiling_yen |
INTEGER | NOT NULL, >= 0 |
上限=前月の給与合計 |
savings_yen |
INTEGER | NOT NULL, default 50000, >= 0 |
固定貯金 |
fixed_costs_yen |
INTEGER | NOT NULL, default 0, >= 0 |
確定時スナップショットの固定費合計 |
variable_pool_yen |
INTEGER | NOT NULL, default 0 | ceiling − savings − fixed_costs |
unallocated_to_surplus_yen |
INTEGER | NOT NULL, default 0 | 月初確定で別枠へ入れた未割当 |
month_end_surplus_yen |
INTEGER | 月末締めで別枠へ入れた実残 | |
status |
TEXT | draft / confirmed / closed |
未確定 / 確定 / 月末締め済み |
created_at |
TIMESTAMPTZ | NOT NULL | |
updated_at |
TIMESTAMPTZ | NOT NULL |
draft 中は、前月給与と固定費マスタから天井と変動枠を取り直す。
budget_month_categories(変動カテゴリ枠)¶
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
year_month |
DATE | PK, FK → budget_months |
対象月 |
category |
TEXT | PK, <> 'その他' |
変動大分類 |
monthly_limit_yen |
INTEGER | NOT NULL, >= 0 |
その月の枠 |
note |
TEXT |
親行削除で CASCADE する。 固定費名(家賃、通信など)はここに入れない。
fixed_cost_items(固定費マスタ)¶
毎月引く定額リストである。 実績支出とは別に「引く額」を持つ。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | |
name |
TEXT | NOT NULL, UNIQUE | 項目名(家賃+駐車場 など) |
monthly_yen |
INTEGER | NOT NULL, >= 0 |
月額 |
expense_category |
TEXT | 実績集計用の expenses.category(任意) |
|
active |
BOOLEAN | NOT NULL, default TRUE | FALSE なら合計から除外 |
note |
TEXT | ||
updated_at |
TIMESTAMPTZ | NOT NULL |
expense_category が付いている支出は、変動枠の消化に入れない。
未設定(例: 散髪)は引くだけである。
surplus_ledger(別枠貯金)¶
固定貯金とは別の袋である。 正は入金、負は出金とする。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | |
year_month |
DATE | 対象月(手動は null 可) | |
amount_yen |
INTEGER | NOT NULL | 金額 |
reason |
TEXT | month_unallocated / month_end_remainder / manual |
理由 |
note |
TEXT | ||
created_at |
TIMESTAMPTZ | NOT NULL |
| reason | いつ増えるか |
|---|---|
month_unallocated |
Discord「このままで」(変動枠のうち未割当) |
month_end_remainder |
Discord「月末締め」(変動の実残) |
manual |
手入力の入出金(取り崩し UI は未実装) |
同じ月、同じ reason は二重に積まない。
wishlist_items / planned_expenses¶
固定貯金、別枠貯金とはさらに別である。
wishlist_items¶
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | |
name |
TEXT | NOT NULL | 名前 |
target_yen |
INTEGER | NOT NULL, > 0 |
目標額 |
saved_yen |
INTEGER | NOT NULL, default 0, >= 0 |
進捗 |
status |
TEXT | open / bought / cancelled |
|
discord_user_id |
TEXT | ||
created_at / updated_at |
TIMESTAMPTZ | NOT NULL |
planned_expenses¶
wishlist に近い。
違うのは deadline(DATE)と status open / done / cancelled である。
notification_raw(通知の生データ)¶
PayPay 等の Android 通知を置く。 自動取込は後段である。
| 列 | 型 | 制約 | 意味 |
|---|---|---|---|
id |
BIGSERIAL | PK | |
source |
TEXT | default android_notification |
|
app / title / body_text |
TEXT | 通知の見た目 | |
received_at |
TIMESTAMPTZ | ||
payload |
JSONB | NOT NULL | 生 JSON |
dedupe_key |
TEXT | UNIQUE(NULL 以外) | 二重取込防止 |
expense_id |
BIGINT | FK → expenses, ON DELETE SET NULL |
支出化できたとき |
skipped_reason |
TEXT | 支出にしなかった理由 | |
created_at |
TIMESTAMPTZ | NOT NULL |
権限¶
Grafana は SELECT のみである。
finance_readonly への GRANT は各 sql/*.sql の末尾、または sql/grafana_readonly_grants.sql にある。