267d292a90
- Varsayılan varlıklar (TL, altınlar, USD, EUR) + migration v2 ile eski hanelere ekleme - Döviz ekleme, varlık adı/aktifliği - Alım/satım ekle, düzenle, sil; eldekinden fazla satış (geriye tarihli dahil) engellenir - Ağırlıklı ortalama maliyet ve gerçekleşen kâr/zarar (hesaplanır, saklanmaz) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
3.4 KiB
Rust
121 lines
3.4 KiB
Rust
//! Basit şema migration mekanizması (DATA_MODEL.md §3.15).
|
||
//!
|
||
//! Her migration bir kez çalışır; `schema_migrations` koleksiyonuna
|
||
//! `{ _id: sürüm, name, applied_at }` olarak kaydedilir.
|
||
//! Yeni migration eklemek: `MIGRATIONS` listesine bir sonraki sürüm numarasıyla ekle.
|
||
|
||
use std::{future::Future, pin::Pin};
|
||
|
||
use mongodb::{
|
||
Database,
|
||
bson::{DateTime, Document, doc},
|
||
error::Result,
|
||
};
|
||
|
||
use super::col;
|
||
|
||
type MigrationFn =
|
||
for<'a> fn(&'a Database) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
|
||
|
||
struct Migration {
|
||
version: i32,
|
||
name: &'static str,
|
||
run: MigrationFn,
|
||
}
|
||
|
||
/// Sıralı migration listesi. Sürümler 1'den başlar ve atlamadan artar.
|
||
const MIGRATIONS: &[Migration] = &[
|
||
Migration {
|
||
version: 1,
|
||
name: "initial",
|
||
run: |db| Box::pin(m001_initial(db)),
|
||
},
|
||
Migration {
|
||
version: 2,
|
||
name: "default_assets",
|
||
run: |db| Box::pin(m002_default_assets(db)),
|
||
},
|
||
];
|
||
|
||
/// Uygulanmamış migration'ları sırayla çalıştırır.
|
||
pub async fn run(db: &Database) -> Result<()> {
|
||
let coll = db.collection::<Document>(col::SCHEMA_MIGRATIONS);
|
||
|
||
for m in MIGRATIONS {
|
||
if coll.find_one(doc! { "_id": m.version }).await?.is_some() {
|
||
continue;
|
||
}
|
||
tracing::info!("Migration çalışıyor: v{} ({})", m.version, m.name);
|
||
(m.run)(db).await?;
|
||
coll.insert_one(doc! {
|
||
"_id": m.version,
|
||
"name": m.name,
|
||
"applied_at": DateTime::now(),
|
||
})
|
||
.await?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// v1: Tüm koleksiyonları açıkça oluşturur.
|
||
/// (Transaction içinde ilk kez yazılan koleksiyonlar sorun çıkarmasın diye önceden var olsunlar.)
|
||
async fn m001_initial(db: &Database) -> Result<()> {
|
||
let existing = db.list_collection_names().await?;
|
||
let all = col::HOUSEHOLD_SCOPED
|
||
.iter()
|
||
.chain(&[col::EXCHANGE_RATES, col::SCHEMA_MIGRATIONS]);
|
||
|
||
for name in all {
|
||
if !existing.iter().any(|e| e == name) {
|
||
db.create_collection(*name).await?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// v2: Faz 8'den önce açılmış hanelere varsayılan varlıklar (TL, altınlar, USD, EUR).
|
||
/// Varlığı olan haneye dokunmaz → tekrar çalışsa da çift kayıt olmaz.
|
||
async fn m002_default_assets(db: &Database) -> Result<()> {
|
||
#[derive(serde::Deserialize)]
|
||
struct Household {
|
||
#[serde(rename = "_id")]
|
||
id: mongodb::bson::Uuid,
|
||
}
|
||
let assets = db.collection::<Document>(col::ASSETS);
|
||
let mut cursor = db
|
||
.collection::<Household>(col::HOUSEHOLDS)
|
||
.find(doc! { "deleted_at": null })
|
||
.projection(doc! { "_id": 1 })
|
||
.await?;
|
||
while cursor.advance().await? {
|
||
let hid = cursor.deserialize_current()?.id;
|
||
if assets
|
||
.find_one(doc! { "household_id": hid })
|
||
.await?
|
||
.is_none()
|
||
{
|
||
assets
|
||
.insert_many(crate::services::seed::assets(hid))
|
||
.await?;
|
||
tracing::info!("Varsayılan varlıklar eklendi: hane {hid}");
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn versions_start_at_1_and_are_sequential() {
|
||
for (i, m) in MIGRATIONS.iter().enumerate() {
|
||
assert_eq!(
|
||
m.version,
|
||
i as i32 + 1,
|
||
"migration sürümleri ardışık olmalı"
|
||
);
|
||
}
|
||
}
|
||
}
|