Files
kakeibo/docs/DATA_MODEL.md
T
2026-11-11 23:05:26 +03:00

229 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🗄️ Veri Modeli (v1)
> Durum: ✅ **Onaylandı (2026-11-11)** · Veritabanı: MongoDB 7 (replica set) · Şema sürümü: `1`
## 1. Genel Kurallar
### 1.1 Ortak alanlar (her koleksiyonda)
| Alan | Tip | Açıklama |
|---|---|---|
| `_id` | UUID v7 (BSON Binary, subtype 4) | Zamana göre sıralanabilir; çevrimdışı cihaz da üretebilir (Faz 11 senkronizasyon) |
| `household_id` | UUID | Hane izolasyonu. **Her sorguda zorunlu filtre** |
| `created_at` | DateTime (UTC) | |
| `updated_at` | DateTime (UTC) | Senkronizasyonda "bundan sonra değişenler" sorgusu |
| `deleted_at` | DateTime \| null | Soft delete. Normal sorgular `deleted_at: null` filtreler |
| `version` | i32 | Her güncellemede +1. Çakışma kontrolü (iki kişi aynı kaydı düzenlerse) |
İstisna: `users` hane bilgisini `household_id` alanında tutar. `exchange_rates` ve `schema_migrations` haneye bağlı değildir (bkz. ilgili bölüm).
### 1.2 Sayısal değerler — float **yok**
| Değer | Saklama | Örnek |
|---|---|---|
| Para (TL) | `i64` **kuruş** | `1.250,00 ₺` → `125000` |
| Varlık miktarı | `i64` **milyonda bir birim** (`_e6`) | `12,5 gr` → `12500000` · `150 $` → `150000000` |
| Kur / birim fiyat | `i64` **milyonda bir TL** (`_e6`) | `1 USD = 41,2345 TL` → `41234500` |
- Hesaplamalarda taşmayı önlemek için ara çarpımlar `i128` ile yapılır: `toplam_kuruş = miktar_e6 × fiyat_e6 / 10¹⁰`.
- **Neden Decimal128 değil?** Rust sürücüsünde Decimal128 ile aritmetik yapılamıyor (önce dönüştürmek gerekiyor) ve aggregation toplamları tam sayılarda daha basit. Ölçekli tam sayı tek tip ve hızlı.
### 1.3 Tarihler
- **Takvim tarihi** (harcama tarihi, dönem başı/sonu, plan kalemi tarihi): `"YYYY-MM-DD"` **metin** (yerel tarih). Saat dilimi kayması olmaz, metin sıralaması = tarih sıralaması.
- **Zaman damgası** (`created_at` vb.): BSON DateTime (UTC).
- Ekranda her zaman `GG.AA.YYYY`.
### 1.4 Hesaplanan değerler saklanmaz
Gerçekleşen toplamlar, kalan tutar, hafta bütçeleri, kâr/zarar, ortalama maliyet → sorgu anında aggregation / `services/` ile hesaplanır.
## 2. Sabit Tanımlar (kodda, veritabanında değil)
**Ana gruplar** (`group` alanı): değiştirilemez, silinemez.
| Kod | Ad | Emoji | Varsayılan % |
|---|---|---|---|
| `needs` | Zorunlu | 🏠 | 50 |
| `wants` | İstek | 🛍️ | 30 |
| `culture` | Kültürel | 📚 | 10 |
| `unexpected` | Beklenmeyen | ⚡ | 10 |
Renkler ve ikonlar frontend tasarım token'larında.
## 3. Koleksiyonlar
### 3.1 `users`
```js
{ _id, household_id, email /* küçük harf, benzersiz */, password_hash /* argon2 */,
name, role: "owner" | "member",
preferences: { theme: "light" | "dark" | "system" },
last_login_at, created_at, updated_at, deleted_at, version }
```
- Bir kullanıcı **tek bir haneye** üyedir.
### 3.2 `households`
```js
{ _id, name, owner_id,
settings: {
currency: "TRY",
budget_split_pct: { needs: 50, wants: 30, culture: 10, unexpected: 10 },
budget_suggestion: "previous" | "avg3" | "default" // varsayılan öneri yöntemi
},
created_at, updated_at, deleted_at, version }
```
- Prompttaki `settings` koleksiyonu yerine **haneye gömülü** (her hanede tek kayıt olduğu için ayrı koleksiyona gerek yok).
### 3.3 `invites`
```js
{ _id, household_id, code /* 8 karakter, benzersiz */, created_by,
expires_at, used_by: null | user_id, used_at, created_at, ... }
```
### 3.4 `periods` — Dönem
```js
{ _id, household_id, name /* "Aralık 2026" */,
start_date: "2026-12-01", end_date: "2027-01-01", // bitiş HARİÇ
status: "open" | "closed",
savings_goal: 500000, // kuruş — birikim hedefi
reflection_md: "...", // "Nasıl daha iyi yapabilirim?"
closed_at, closed_by, reopened_at,
previous_period_id,
created_at, updated_at, deleted_at, version }
```
- **Bitiş tarihi hariç:** `01.12.2026 – 01.01.2027` → 1–31 Aralık. Dönemler arka arkaya boşluksuz ve çakışmasız eklenir.
- Hanede en fazla **bir** `open` dönem (servis kontrolü + kısmi benzersiz indeks).
- Yeniden açılan dönemin durumu tekrar `open` olur. O sırada başka açık dönem varsa önce o kapatılmalı mı, yoksa geçici olarak iki açık dönem mi olsun? → **Açık konu, Faz 6'da netleşecek.**
- **Haftalar saklanmaz.** `start_date`/`end_date`'ten hesaplanır (Pazartesi başlangıçlı; ilk/son kısmi haftalar ayrı).
### 3.5 `categories` — Alt kategoriler
```js
{ _id, household_id, group: "needs", name: "Market", emoji: "🛒",
sort_order: 10, is_active: true, created_at, ... }
```
- Silinmez, `is_active: false` ile pasife alınır.
- İlk kurulumda örnekler: **Zorunlu:** Market, Kira, Faturalar, Ulaşım · **İstek:** Dışarıda yemek, Giyim, Eğlence · **Kültürel:** Kitap, Eğitim, Sinema/Konser · **Beklenmeyen:** Sağlık, Tamir, Hediye
### 3.6 `payment_methods`
```js
{ _id, household_id, name: "Kredi Kartı", emoji: "💳",
sort_order, is_active, created_at, ... }
```
- İlk kurulumda: 💵 Nakit, 💳 Kredi Kartı, 🏧 Banka Kartı, 🔁 Havale/EFT
### 3.7 `plan_items` — Sabit gelir / gider kalemleri
```js
{ _id, household_id, period_id,
kind: "income" | "expense",
name: "Maaş", amount: 4500000, date: "2026-12-05",
sort_order, copied_from_id /* önceki dönemden kopyalandıysa */,
created_at, ... }
```
### 3.8 `budgets` — Grup bütçeleri
```js
{ _id, household_id, period_id, group: "needs",
scope: "month" | "week",
week_start: null | "2026-12-07", // scope = week ise o haftanın ilk günü
amount: 1500000,
created_at, ... }
```
- Her dönem × grup için **bir aylık** kayıt.
- Haftalık bütçe normalde **saklanmaz**, aylık tutardan gün sayısına göre hesaplanır. Yalnızca kullanıcı haftalık tutarı **elle değiştirirse** `scope: "week"` kaydı oluşur.
### 3.9 `entries` — Harcamalar (günlük defter)
```js
{ _id, household_id, period_id,
date: "2026-12-09", amount: 34550,
group: "needs", category_id, payment_method_id,
description: "Haftalık market",
created_by /* user_id */, created_at, ... }
```
### 3.10 `carryovers` — Devirler
```js
{ _id, household_id, from_period_id, to_period_id,
kind: "debt" | "surplus", amount: 120000,
created_at, ... }
```
- Dönem kapanışında oluşur. Dönem yeniden açılırsa soft delete edilir ve kapanışta yeniden hesaplanır.
### 3.11 `notes`
```js
{ _id, household_id, period_id,
context: "planning" | "review",
title, body_md, created_by, created_at, ... }
```
### 3.12 `assets` — Varlıklar
```js
{ _id, household_id,
kind: "cash" | "gold" | "fx",
code: "TRY" | "XAU_GRAM" | "XAU_CEYREK" | "XAU_YARIM" | "XAU_TAM" | "XAU_ONS" | "USD" | "EUR" | ...,
name: "Gram Altın", unit_label: "gr",
is_active, created_at, ... }
```
### 3.13 `asset_transactions` — Alım / satım
```js
{ _id, household_id, asset_id,
type: "buy" | "sell",
date: "2026-12-15",
quantity_e6: 5000000, // 5 gr
unit_price_e6: 3250000000, // 3.250,00 TL
total: 1625000, // kuruş (girilen gerçek tutar; komisyon farkı olabilir)
note, source_period_id /* dönem kapanışından geldiyse */,
created_at, ... }
```
- Ortalama maliyet ve kâr/zarar bu kayıtlardan hesaplanır, saklanmaz.
### 3.14 `exchange_rates` — Kurlar
```js
{ _id, household_id: null | id, // null = otomatik (tüm haneler), dolu = hanenin elle girdiği kur
code: "USD", rate_e6: 41234500,
source: "tcmb" | "gold_api" | "manual",
rate_date: "2026-12-15", fetched_at,
created_at, ... }
```
- Güncel kur sırası: hanenin elle girdiği en yeni kur → en yeni otomatik kur.
### 3.15 `schema_migrations`
```js
{ _id: 1 /* sürüm numarası */, name: "initial", applied_at }
```
- Uygulama açılışında uygulanmamış migration'lar sırayla çalışır, ardından indeksler oluşturulur.
## 4. İndeksler
| Koleksiyon | İndeks | Tip |
|---|---|---|
| `users` | `email` | benzersiz |
| `invites` | `code` | benzersiz |
| `periods` | `(household_id, start_date)` | |
| `periods` | `(household_id)` where `status: "open"` | kısmi benzersiz |
| `categories`, `payment_methods` | `(household_id, sort_order)` | |
| `plan_items`, `budgets`, `notes` | `(household_id, period_id)` | |
| `entries` | `(household_id, period_id, date)` | |
| `entries` | `(household_id, date)` | raporlar için |
| `carryovers` | `(household_id, to_period_id)` | |
| `asset_transactions` | `(household_id, asset_id, date)` | |
| `exchange_rates` | `(code, household_id, rate_date desc)` | |
| *(tümü)* | `(household_id, updated_at)` | senkronizasyon (Faz 11) |
## 5. Temel Hesaplamalar (servis katmanı)
| Değer | Formül |
|---|---|
| **Harcanabilir tutar** | Σ gelir − Σ sabit gider − birikim hedefi − devreden borç + devreden artı |
| **Gerçekleşen harcama** | Σ `entries.amount` (dönem içi) |
| **Kalan harcanabilir** | Harcanabilir tutar − gerçekleşen harcama |
| **Haftalık bütçe** | aylık grup bütçesi × (haftanın dönem içindeki gün sayısı ÷ dönemin gün sayısı), elle girilmiş kayıt varsa o |
| **Gerçekleşen birikim** (4. soru) | Σ gelir − Σ sabit gider − devreden borç + devreden artı − gerçekleşen harcama |
| **Kapanışta devir** | Gerçekleşen birikim < 0 → `debt` · > 0 ise kullanıcı seçer: birikime aktar veya `surplus` olarak devret |
| **Ortalama maliyet** | Hareketli ağırlıklı ortalama: alımda güncellenir, satışta değişmez |
| **Kâr/zarar** | (eldeki miktar × güncel kur) − (eldeki miktar × ortalama maliyet) |
## 6. Dönem Kapanışı (tek transaction)
1. `periods.status = "closed"`, `closed_at`, `closed_by`
2. `carryovers` kaydı (borç veya artı) → sonraki dönem
3. (Onaylıysa) gerçekleşen birikim → `asset_transactions` (`TRY`, `buy`, `source_period_id`)
4. (İstenirse) yeni dönem: `plan_items` ve aylık `budgets` kopyalanarak açılır
Transaction'ın bir adımı hata verirse önceki adımlar da geri alınır, hiçbir şey değişmez.