docs(veri-modeli): veri modeli v1 onaylandı, Faz 0 tamamlandı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 23:05:26 +03:00
parent af2c01dbee
commit 306c550b28
3 changed files with 242 additions and 4 deletions
+228
View File
@@ -0,0 +1,228 @@
# 🗄️ 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.