Files
suphicakir 48554e0ff6 feat(birikim): dönem kapanışındaki birikimi varlıklara ekleme
- Kapatma sonucunda onayla "Birikime ekle" (TL alımı, dönemin son günü)
- Birikimler'de "Bekleyen birikimler" listesi
- Yeniden kapatmada bağlı kayıt yeni tutara güncellenir (0 → silinir), TL çıkışları aşılırsa reddedilir
- Panel "Toplam birikim" artık portföyün güncel değeri

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 01:27:07 +03:00

238 lines
12 KiB
Markdown
Raw Permalink 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, 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`.
- **Kapanıştan birikim:** kullanıcı onaylayınca (`POST /periods/{id}/to-savings`) dönemin `to_savings` tutarı TL varlığına `buy` olarak eklenir: tarih = dönemin son günü, `source_period_id` = dönem; dönem başına tek. Eklenmemiş kapalı dönemler `GET /savings/pending`. **Yeniden kapatmada** bağlı kayıt yeni tutar ve tarihe güncellenir (tutar 0 → silinir), kapatma transaction'ı içinde; TL'den sonraki çıkışlar yeni tutarı aşacaksa kapatma reddedilir (kullanıcı kararı "a", 2026-11-13). Bağlı kayıt yoksa otomatik eklenmez (onay gerekir).
### 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: kod başına **en yeni `rate_date`**; aynı gün hem otomatik hem manuel varsa manuel. (Eski bir manuel kur, daha yeni otomatik kurun önüne geçmez.) Kur = 1 birimin TL karşılığı × 1e6; döviz **alış** kuru (değerleme).
- Otomatik kurlar gün başına tek kayıt (kod × kaynak × gün; aynı gün yeniden çekilince güncellenir) → günlük geçmiş birikir. TCMB `rate_date` = bülten tarihi; altın = Türkiye'de bugün.
- Altın: xaus.com gram (saf) USD × TCMB USD alış; çeyrek/yarım/tam = has gram (1,603 / 3,206 / 6,412) × gram, ons = 31,1034768 gr. İşçilik payı yok → gerekirse manuel.
### 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.