Compare commits
3 Commits
af2c01dbee
...
8ec24ecec2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec24ecec2 | |||
| 125bb7f476 | |||
| 306c550b28 |
Generated
+1792
-5
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,10 @@ edition = "2024"
|
||||
[dependencies]
|
||||
axum = "0.8.9"
|
||||
dotenvy = "0.15.7"
|
||||
mongodb = "3.10.0"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.154"
|
||||
thiserror = "2.0.21"
|
||||
tokio = { version = "1.54.1", features = ["full"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
|
||||
@baseUrl = http://localhost:8080/api
|
||||
|
||||
### Sağlık kontrolü
|
||||
### Sağlık kontrolü (DB erişilemezse 503 + "db": "error")
|
||||
GET {{baseUrl}}/health
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Ortam değişkenlerinden uygulama ayarlarını okur.
|
||||
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub mongodb_uri: String,
|
||||
#[allow(dead_code)] // Faz 1 – Adım 3'te (JWT) kullanılacak
|
||||
pub jwt_secret: String,
|
||||
pub app_url: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("{0} ortam değişkeni tanımlı değil (.env dosyasını kontrol edin)")]
|
||||
Missing(&'static str),
|
||||
#[error("{0} geçersiz: {1}")]
|
||||
Invalid(&'static str, &'static str),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result<Self, ConfigError> {
|
||||
let mongodb_uri = required("MONGODB_URI")?;
|
||||
|
||||
let jwt_secret = required("JWT_SECRET")?;
|
||||
if jwt_secret.len() < 32 {
|
||||
return Err(ConfigError::Invalid(
|
||||
"JWT_SECRET",
|
||||
"en az 32 karakter olmalı (openssl rand -hex 32)",
|
||||
));
|
||||
}
|
||||
|
||||
let app_url = env::var("APP_URL").unwrap_or_else(|_| "http://localhost:5173".into());
|
||||
|
||||
let port = match env::var("PORT") {
|
||||
Ok(p) => p
|
||||
.parse()
|
||||
.map_err(|_| ConfigError::Invalid("PORT", "sayı olmalı"))?,
|
||||
Err(_) => 8080,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
mongodb_uri,
|
||||
jwt_secret,
|
||||
app_url,
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cookie'lerde `Secure` bayrağı yalnızca HTTPS'te açılır.
|
||||
#[allow(dead_code)] // Faz 1 – Adım 3'te (kimlik doğrulama) kullanılacak
|
||||
pub fn is_https(&self) -> bool {
|
||||
self.app_url.starts_with("https://")
|
||||
}
|
||||
}
|
||||
|
||||
fn required(key: &'static str) -> Result<String, ConfigError> {
|
||||
env::var(key)
|
||||
.ok()
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.ok_or(ConfigError::Missing(key))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//! MongoDB bağlantısı.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use mongodb::{Client, Database, bson::doc, options::ClientOptions};
|
||||
|
||||
/// URI'de veritabanı adı yoksa kullanılacak ad.
|
||||
const DEFAULT_DB_NAME: &str = "kakeibo";
|
||||
|
||||
/// Bağlantıyı kurar ve bir `ping` ile sunucunun gerçekten erişilebilir olduğunu doğrular.
|
||||
pub async fn connect(uri: &str) -> mongodb::error::Result<Database> {
|
||||
let mut options = ClientOptions::parse(uri).await?;
|
||||
options.app_name = Some("kakeibo".into());
|
||||
// Sunucu yoksa 30 sn yerine 5 sn içinde hata ver.
|
||||
options.server_selection_timeout = Some(Duration::from_secs(5));
|
||||
|
||||
let db_name = options
|
||||
.default_database
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_DB_NAME.into());
|
||||
|
||||
let client = Client::with_options(options)?;
|
||||
let db = client.database(&db_name);
|
||||
ping(&db).await?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub async fn ping(db: &Database) -> mongodb::error::Result<()> {
|
||||
db.run_command(doc! { "ping": 1 }).await.map(|_| ())
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Uygulama genelindeki hata türü.
|
||||
//! Kullanıcıya Türkçe ve sade mesaj döner; teknik detay yalnızca loga yazılır.
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("Kayıt bulunamadı")]
|
||||
NotFound,
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
#[error("Oturum açmanız gerekiyor")]
|
||||
Unauthorized,
|
||||
#[error("Bu işlem için yetkiniz yok")]
|
||||
Forbidden,
|
||||
#[error("{0}")]
|
||||
Conflict(String),
|
||||
#[error("Çok fazla deneme yapıldı, lütfen biraz sonra tekrar deneyin")]
|
||||
TooManyRequests,
|
||||
#[error("veritabanı hatası: {0}")]
|
||||
Database(#[from] mongodb::error::Error),
|
||||
#[error("iç hata: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
fn status_and_code(&self) -> (StatusCode, &'static str) {
|
||||
match self {
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, "not_found"),
|
||||
Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
|
||||
Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
|
||||
Self::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
|
||||
Self::TooManyRequests => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests"),
|
||||
Self::Database(_) | Self::Internal(_) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, code) = self.status_and_code();
|
||||
|
||||
let message = if status.is_server_error() {
|
||||
// Teknik detay sadece logda; kullanıcıya genel mesaj.
|
||||
tracing::error!(error = %self, "sunucu hatası");
|
||||
"Beklenmeyen bir hata oluştu, lütfen tekrar deneyin".to_string()
|
||||
} else {
|
||||
self.to_string()
|
||||
};
|
||||
|
||||
(
|
||||
status,
|
||||
Json(json!({ "error": { "code": code, "message": message } })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Route fonksiyonlarında kısa dönüş tipi: `AppResult<Json<T>>`
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
+53
-18
@@ -1,11 +1,18 @@
|
||||
use std::net::SocketAddr;
|
||||
mod config;
|
||||
mod db;
|
||||
#[allow(dead_code)] // AppError, Faz 1 – Adım 3 itibarıyla route'larda kullanılacak
|
||||
mod error;
|
||||
mod routes;
|
||||
mod state;
|
||||
|
||||
use std::{net::SocketAddr, process::ExitCode, sync::Arc};
|
||||
|
||||
use axum::{Json, Router, routing::get};
|
||||
use serde_json::{Value, json};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{config::Config, state::AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> ExitCode {
|
||||
// .env dosyasını yükle (proje kökünde; üst klasörler de aranır). Yoksa sorun değil.
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
@@ -15,22 +22,50 @@ async fn main() {
|
||||
)
|
||||
.init();
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(8080);
|
||||
let config = match Config::from_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Ayar hatası: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let app = Router::new().route("/api/health", get(health));
|
||||
let db = match db::connect(&config.mongodb_uri).await {
|
||||
Ok(db) => {
|
||||
tracing::info!("MongoDB bağlantısı kuruldu (veritabanı: {})", db.name());
|
||||
db
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"MongoDB'ye bağlanılamadı. MONGODB_URI doğru mu, veritabanı çalışıyor mu? \
|
||||
(yerelde: npm run db:up)\nDetay: {e}"
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let port = config.port;
|
||||
let state = AppState {
|
||||
db,
|
||||
config: Arc::new(config),
|
||||
};
|
||||
let app = routes::router(state);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
.expect("Port dinlenemedi");
|
||||
tracing::info!("Kakeibo API çalışıyor: http://localhost:{port}");
|
||||
axum::serve(listener, app).await.expect("Sunucu hatası");
|
||||
}
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"{port} portu dinlenemedi (başka bir uygulama kullanıyor olabilir): {e}"
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
/// Sağlık kontrolü. Faz 1'de veritabanı durumu da eklenecek.
|
||||
async fn health() -> Json<Value> {
|
||||
Json(json!({ "status": "ok", "version": env!("CARGO_PKG_VERSION") }))
|
||||
tracing::info!("Kakeibo API çalışıyor: http://localhost:{port}");
|
||||
if let Err(e) = axum::serve(listener, app).await {
|
||||
tracing::error!("Sunucu hatası: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{db, state::AppState};
|
||||
|
||||
/// Sağlık kontrolü (Dokploy health check bunu kullanır).
|
||||
/// DB erişilemezse 503 döner.
|
||||
pub async fn health(State(state): State<AppState>) -> (StatusCode, Json<Value>) {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
match db::ping(&state.db).await {
|
||||
Ok(()) => (
|
||||
StatusCode::OK,
|
||||
Json(json!({ "status": "ok", "db": "ok", "version": version })),
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "health: veritabanına erişilemiyor");
|
||||
(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(json!({ "status": "degraded", "db": "error", "version": version })),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! API route'larının tek giriş noktası. Tüm uç noktalar `/api` altında.
|
||||
|
||||
mod health;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub fn router(state: AppState) -> Router {
|
||||
let api = Router::new().route("/health", get(health::health));
|
||||
|
||||
Router::new().nest("/api", api).with_state(state)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Tüm route'ların paylaştığı uygulama durumu.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use mongodb::Database;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
/// `Clone` ucuzdur: `Database` ve `Arc` içte referans sayımı kullanır, veri kopyalanmaz.
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Database,
|
||||
#[allow(dead_code)] // Faz 1 – Adım 3'te kullanılacak
|
||||
pub config: Arc<Config>,
|
||||
}
|
||||
@@ -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.
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
# 🧠 Proje Hafızası
|
||||
|
||||
## ▶️ Sıradaki Adım
|
||||
**Faz 0 – Adım 5:** Veri modeli taslağı `docs/DATA_MODEL.md` → kullanıcı onayı. Onaydan sonra Faz 0 kapanır, Faz 1 başlar.
|
||||
**Faz 1 – Adım 2:** Ortak model alanları (UUID v7, zaman damgaları, version), `schema_migrations` ile migration mekanizması, `DATA_MODEL.md` §4 indeksleri (açılışta otomatik).
|
||||
|
||||
## 📌 Alınan Kararlar
|
||||
- Sürüm kontrolü: Gitea (`https://gitea.ta1sph.info/suphicakir/kakeibo.git`), dal `main`.
|
||||
@@ -10,9 +10,11 @@
|
||||
- Git kimlik bilgisi: libsecret (KWallet) credential helper + Gitea token.
|
||||
- Yerel MongoDB host portu 27020 (27017 sistem mongod, 27018/27019 başka projeler).
|
||||
- Frontend lint aracı: oxlint (Vite şablonu varsayılanı), ESLint yerine.
|
||||
- Veri modeli v1 onaylandı (2026-11-11): ölçekli tam sayı (`_e6`), tarih `YYYY-MM-DD` metin, dönem bitişi hariç, kullanıcı = tek hane, ayarlar haneye gömülü, haftalık bütçe hesaplanır.
|
||||
- Sürümler: Rust 1.99.1 (edition 2024), Axum 0.8, React 19, Vite 8, TypeScript 7.
|
||||
|
||||
## ❓ Açık Konular
|
||||
- Kapalı dönem yeniden açılırken başka açık dönem varsa davranış (Faz 6).
|
||||
- Altın kuru için veri kaynağı seçimi (Faz 8).
|
||||
- Aylık değerlendirme çıktı formatı kullanıcıdan gelecek (Faz 9).
|
||||
- Canlı MongoDB kurulum şekli (konteyner mi, doğrudan mı) Faz 3'te birlikte kontrol edilecek.
|
||||
@@ -24,3 +26,7 @@
|
||||
- **Adım 2:** `.vscode/` (extensions, settings, tasks, launch), `.gitignore`, `.env.example` ve monorepo klasörleri oluşturuldu. `git init -b main`.
|
||||
- **Adım 3:** `CLAUDE.md`, `docs/MEMORY.md`, `docs/PHASES.md` oluşturuldu. Gitea remote eklendi, ilk commit push edildi.
|
||||
- **Adım 4:** Rust kuruldu. `docker-compose.dev.yml` (mongo:7, rs0, healthcheck ile otomatik `rs.initiate`). Backend: Axum, yalnızca `GET /api/health`. Frontend: Vite react-ts şablonu, `/api` proxy → 8080, geçici durum ekranı. Kök `package.json`: concurrently ile `npm run dev`. Uçtan uca test edildi (Mongo PRIMARY, proxy çalışıyor).
|
||||
- **Adım 5:** `docs/DATA_MODEL.md` hazırlandı ve onaylandı. **Faz 0 tamamlandı.**
|
||||
|
||||
### 2026-11-11 — Faz 1
|
||||
- **Adım 1:** `config.rs` (env + doğrulama, JWT_SECRET ≥ 32), `db.rs` (5 sn timeout + ping), `error.rs` (`AppError` → `{error:{code,message}}` Türkçe; 5xx detayı yalnızca logda), `state.rs`, `routes/` (health: DB yoksa 503). Açılışta DB/ayar hatasında anlaşılır log + çıkış kodu 1. Elle test: 4 senaryo geçti.
|
||||
|
||||
+10
-3
@@ -4,8 +4,8 @@
|
||||
|
||||
| Faz | Kapsam | Çıktı | Durum |
|
||||
|---|---|---|---|
|
||||
| **Faz 0** | Ortam kurulumu, VS Code yapılandırması, monorepo iskeleti, hafıza dosyaları, yerel MongoDB, veri modeli onayı | `npm run dev` ile boş uygulama tarayıcıda açılıyor | 🔄 |
|
||||
| **Faz 1** | Backend temeli: config, DB bağlantısı, kimlik doğrulama, hane, kategoriler, ödeme türleri, health check | API `.http` dosyasıyla test edilebiliyor | ⏳ |
|
||||
| **Faz 0** | Ortam kurulumu, VS Code yapılandırması, monorepo iskeleti, hafıza dosyaları, yerel MongoDB, veri modeli onayı | `npm run dev` ile boş uygulama tarayıcıda açılıyor | ✅ |
|
||||
| **Faz 1** | Backend temeli: config, DB bağlantısı, kimlik doğrulama, hane, kategoriler, ödeme türleri, health check | API `.http` dosyasıyla test edilebiliyor | 🔄 |
|
||||
| **Faz 2** | Frontend temeli: tasarım sistemi, tema, logo/ikonlar, layout, navigasyon, giriş/kayıt | Giriş yapılabilen, temalı iskelet uygulama | ⏳ |
|
||||
| **Faz 3** | 🚀 İlk canlıya alma: Dockerfile, Dokploy, canlı MongoDB, domain + HTTPS, yedekleme | Giriş ekranı canlıda çalışıyor | ⏳ |
|
||||
| **Faz 4** | Dönem açma + aylık planlama + notlar | Plan ekranı | ⏳ |
|
||||
@@ -22,4 +22,11 @@
|
||||
- ✅ 2. `.vscode/` yapılandırması ve monorepo iskeleti
|
||||
- ✅ 3. Hafıza dosyaları (`CLAUDE.md`, `MEMORY.md`, `PHASES.md`) + Git remote
|
||||
- ✅ 4. Yerel MongoDB (Docker, replica set) + `npm run dev`
|
||||
- ⏳ 5. Veri modeli taslağı (`docs/DATA_MODEL.md`) → onay
|
||||
- ✅ 5. Veri modeli taslağı (`docs/DATA_MODEL.md`) → onaylandı
|
||||
|
||||
## Faz 1 Adımları
|
||||
- ✅ 1. `config.rs` + `db.rs` (MongoDB bağlantısı), Türkçe hata yapısı, health check'e DB durumu
|
||||
- 🔄 2. Modeller, migration mekanizması ve indeksler
|
||||
- ⏳ 3. Kimlik doğrulama: kayıt (+ hane + örnek kategori/ödeme türü), giriş, çıkış, `me`; argon2 + JWT cookie + rate limit
|
||||
- ⏳ 4. Hane izolasyonu (middleware), üye davet kodu
|
||||
- ⏳ 5. Kategori ve ödeme türü uç noktaları + hane izolasyonu testleri, `api.http`
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"postinstall": "npm --prefix frontend install",
|
||||
"predev": "npm run db:up",
|
||||
"dev": "concurrently -k -n api,web -c blue,green \"npm:dev:api\" \"npm:dev:web\"",
|
||||
"dev:api": "cd backend && cargo watch -q -c -x run",
|
||||
"dev:api": "cd backend && cargo watch -q -c -w src -w Cargo.toml -x run",
|
||||
"dev:web": "npm --prefix frontend run dev",
|
||||
"predev:host": "npm run db:up",
|
||||
"dev:host": "concurrently -k -n api,web -c blue,green \"npm:dev:api\" \"npm --prefix frontend run dev -- --host\"",
|
||||
|
||||
Reference in New Issue
Block a user