# 🗄️ 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, household_id /* = _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 } ``` - `household_id` = `_id` (ortak `scope()` filtresi tüm koleksiyonlarda aynı çalışsın diye). - 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" | "correcting", savings_goal: 500000, // kuruş — birikim hedefi reflection_md: "...", // "Nasıl daha iyi yapabilirim?" to_savings: 450000, // kapanışta birikime aktarılan (Faz 8'de varlık hareketi) closed_at, closed_by, reopened_at, reopened_by, 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çma (karar 2026-11-12, "düzeltme modu"):** sahip kapalı dönemi açınca durum `correcting` olur; yazılabilir ama "açık dönem" sayılmaz → yeni dönem açık kalabilir (tek `open` kuralı korunur). Mevcut devir "yeniden kapat"a kadar yerinde kalır; yeniden kapatınca eskisi soft delete, yenisi hesaplanır. Ardından kapanmış dönem varsa açılamaz (zincirleme devirler bozulmasın). - **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. Sonraki dönem henüz yoksa `to_period_id: null`; o dönem açılınca (aynı transaction'da) bağlanır. - Yeniden kapatmada eskisi soft delete edilir, yenisi yazılır (düzeltme süresince eskisi geçerli kalı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", sort_order, is_active, created_at, ... } ``` - Varsayılanlar hane açılınca gelir (migration v2 eski hanelere ekler): TRY, XAU_GRAM, XAU_CEYREK, XAU_YARIM, XAU_TAM, XAU_ONS, USD, EUR. Kullanıcı yalnızca **döviz** (`fx`, 3 harfli kod) ekler; kod hanede tekil. Pasif varlık listede kalır, yeni işlem almaz. ### 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: **ağırlıklı ortalama maliyet** (satışta çıkan maliyet = maliyet × satılan / eldeki; gerçekleşen kâr/zarar = satış tutarı − çıkan maliyet). Maliyet `total`'dan hesaplanır (birim fiyat bilgi amaçlı; verilmezse `total ÷ quantity`). - Tarih sırasıyla (aynı gün: ekleniş sırası) hiçbir anda eldeki miktar eksiye düşemez; ekleme, düzenleme ve silmede yeniden denetlenir. - TL varlığında `quantity_e6 = total × 10.000`, `unit_price_e6 = 1.000.000`. ### 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. Gerçekleşen birikimin `to_savings` kadarı birikime: şimdilik `periods.to_savings`; Faz 8'de → `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.