//! CSV dışa / içe aktarma: biçim, deneme → aktarma, satır hataları, tekrar atlama, //! varlıkta tarih sırasıyla eldekinden fazla satış denetimi. mod common; use axum::http::StatusCode; use common::{Session, TestApp}; use serde_json::{Value, json}; async fn id_by(app: &TestApp, s: &Session, path: &str, key: &str, value: &str) -> String { let (_, list) = app.get(path, s).await; list.as_array() .unwrap() .iter() .find(|c| c[key] == value) .unwrap()["id"] .as_str() .unwrap() .to_string() } async fn open(app: &TestApp, s: &Session, start: &str, end: &str) -> String { let (st, p) = app .post( "/periods", s, json!({ "start_date": start, "end_date": end }), ) .await; assert_eq!(st, StatusCode::CREATED, "{p}"); p["id"].as_str().unwrap().to_string() } async fn import(app: &TestApp, s: &Session, kind: &str, csv: &str, commit: bool) -> Value { let (st, r) = app .post_text(&format!("/import/{kind}?commit={commit}"), s, csv) .await; assert_eq!(st, StatusCode::OK, "{r}"); r } fn errors(r: &Value) -> Vec<(u64, String)> { r["errors"] .as_array() .unwrap() .iter() .map(|e| { ( e["line"].as_u64().unwrap(), e["message"].as_str().unwrap().to_string(), ) }) .collect() } #[tokio::test] async fn entries_export_and_import() { let app = TestApp::new().await; let s = app.register("Test", None).await; let market = id_by(&app, &s, "/categories", "name", "Market").await; let nakit = id_by(&app, &s, "/payment-methods", "name", "Nakit").await; // Ekim kapalı, Kasım açık let oct = open(&app, &s, "2026-10-01", "2026-11-01").await; app.post(&format!("/periods/{oct}/close"), &s, json!({})) .await; let nov = open(&app, &s, "2026-11-01", "2026-12-01").await; app.post(&format!("/periods/{nov}/entries"), &s, json!({ "date": "2026-11-05", "amount": 125_050, "category_id": market, "payment_method_id": nakit, "description": "Kafe; tatlı" })).await; let (st, ct, csv) = app.get_text("/export/entries.csv", &s).await; assert_eq!(st, StatusCode::OK); assert!(ct.starts_with("text/csv"), "{ct}"); assert_eq!( csv, "\u{FEFF}Tarih;Tutar;Grup;Kategori;Ödeme türü;Açıklama\r\n05.11.2026;1250,50;Zorunlu;Market;Nakit;\"Kafe; tatlı\"\r\n" ); // Aynı dosya geri yüklenirse çift kayıt olmaz let r = import(&app, &s, "entries", &csv, true).await; assert_eq!( ( r["total"].as_u64(), r["valid"].as_u64(), r["duplicates"].as_u64(), r["imported"].as_u64() ), (Some(1), Some(0), Some(1), Some(0)) ); // Sütun sırası serbest, ayrı yazımlar; hatalı satırlar raporlanır let file = "Açıklama;Kategori;Tutar;Tarih;Grup;Ödeme türü\r\n\ Ekmek;market;45,90;06.11.2026;;\r\n\ x;Market;10;31.02.2026;;\r\n\ x;Market;10;15.09.2026;;\r\n\ x;Market;10;15.10.2026;;\r\n\ x;Uzay;10;06.11.2026;;\r\n\ x;Market;0;06.11.2026;;\r\n\ x;Market;10;06.11.2026;;Kripto\r\n\ x;Market;10;06.11.2026;İstek;\r\n\ Kitaplar;Kitap;1.200;2026-11-07;Kültürel;nakit\r\n\ Kafe; tatlı;Market;1.250,50;05.11.2026;Zorunlu;Nakit\r\n"; // Son satırda tırnaksız ';' → sütunlar kayar: açıkça hata olmalı let dry = import(&app, &s, "entries", file, false).await; assert_eq!(dry["total"], 10); assert_eq!(dry["valid"], 2); assert_eq!(dry["imported"], 0, "deneme yazmaz"); assert_eq!( errors(&dry), vec![ (3, "Tarih geçersiz: 31.02.2026".into()), (4, "Bu tarih için dönem yok".into()), (5, "Dönem kapalı (Ekim 2026)".into()), (6, "Kategori bulunamadı: Uzay".into()), (7, "Tutar sıfırdan büyük olmalı".into()), (8, "Ödeme türü bulunamadı: Kripto".into()), (9, "Kategori bulunamadı: Market".into()), (11, "Tarih geçersiz: 1.250,50".into()), ] ); assert_eq!( dry["preview"][0], json!(["06.11.2026", "45,90", "Market", "Ekmek"]) ); let (_, before) = app.get(&format!("/periods/{nov}/entries"), &s).await; assert_eq!(before.as_array().unwrap().len(), 1); let done = import(&app, &s, "entries", file, true).await; assert_eq!(done["imported"], 2); let (_, list) = app.get(&format!("/periods/{nov}/entries"), &s).await; let list = list.as_array().unwrap(); assert_eq!(list.len(), 3); let book = list .iter() .find(|e| e["description"] == "Kitaplar") .unwrap(); assert_eq!(book["amount"], 120_000, "1.200 → bin iki yüz"); assert_eq!(book["group"], "culture"); assert_eq!( book["payment_method_id"], nakit.as_str(), "ad büyük/küçük harf duyarsız" ); // Dosyada 2 aynı satır, veritabanında 1 → biri eklenir let twice = "Tarih;Tutar;Kategori;Açıklama\r\n06.11.2026;45,90;Market;Ekmek\r\n06.11.2026;45,90;Market;Ekmek\r\n"; let r = import(&app, &s, "entries", twice, true).await; assert_eq!( (r["duplicates"].as_u64(), r["imported"].as_u64()), (Some(1), Some(1)) ); // Biçim hataları let (st, b) = app .post_text( "/import/entries?commit=false", &s, "Tarih;Tutar\r\n01.11.2026;5\r\n", ) .await; assert_eq!(st, StatusCode::BAD_REQUEST); assert_eq!(b["error"]["message"], "Gerekli sütun eksik: Kategori"); let (st, _) = app.post_text("/import/entries?commit=false", &s, "").await; assert_eq!(st, StatusCode::BAD_REQUEST); let (st, _) = app.post_text("/import/bilinmeyen", &s, "a\r\n").await; assert_eq!(st, StatusCode::NOT_FOUND); // Başka hane yalnızca kendi verisini dışa aktarır let other = app.register("Diger", None).await; let (_, _, csv) = app.get_text("/export/entries.csv", &other).await; assert_eq!(csv.lines().count(), 1, "yalnızca başlık"); app.cleanup().await; } #[tokio::test] async fn plan_items_roundtrip() { let app = TestApp::new().await; let s = app.register("Test", None).await; let nov = open(&app, &s, "2026-11-01", "2026-12-01").await; app.post( &format!("/periods/{nov}/plan-items"), &s, json!({ "kind": "income", "name": "Maaş", "amount": 4_500_000 }), ) .await; app.post( &format!("/periods/{nov}/plan-items"), &s, json!({ "kind": "expense", "name": "Kira", "amount": 1_500_000, "date": "2026-11-05" }), ) .await; let (_, _, csv) = app.get_text("/export/plan-items.csv", &s).await; assert_eq!( csv, "\u{FEFF}Dönem başlangıcı;Dönem;Tür;Ad;Tutar;Tarih\r\n01.11.2026;Kasım 2026;Gelir;Maaş;45000,00;\r\n01.11.2026;Kasım 2026;Sabit gider;Kira;15000,00;05.11.2026\r\n" ); // Başka hane: aynı dönemi açıp içe aktarır let other = app.register("Diger", None).await; let onov = open(&app, &other, "2026-11-01", "2026-12-01").await; let with_errors = format!( "{csv}01.11.2026;Kasım 2026;Prim;Bonus;100,00;\r\n01.01.2026;;Gelir;Eski;1,00;\r\n" ); let r = import(&app, &other, "plan-items", &with_errors, true).await; assert_eq!(r["imported"], 2); assert_eq!( errors(&r), vec![ (4, "Tür \"Gelir\" ya da \"Sabit gider\" olmalı: Prim".into()), (5, "Bu tarih için dönem yok".into()), ] ); let (_, items) = app .get(&format!("/periods/{onov}/plan-items"), &other) .await; assert_eq!(items.as_array().unwrap().len(), 2); // Tekrar → atlanır let r = import(&app, &other, "plan-items", &csv, true).await; assert_eq!( (r["duplicates"].as_u64(), r["imported"].as_u64()), (Some(2), Some(0)) ); app.cleanup().await; } #[tokio::test] async fn asset_transactions_in_date_order() { let app = TestApp::new().await; let s = app.register("Test", None).await; // Satış dosyada alımdan önce ama tarihi sonra → geçerli; fazla satış → hata let file = "Tarih;Varlık kodu;İşlem;Miktar;Birim fiyat;Tutar;Not\r\n\ 10.11.2026;XAU_GRAM;Satım;4;;26.000;\r\n\ 01.11.2026;XAU_GRAM;Alım;10;6.300;63.000;Kuyumcu\r\n\ 12.11.2026;XAU_GRAM;Satış;7;;45.000;\r\n\ 02.11.2026;TRY;Alım;;;5.000;\r\n\ 02.11.2026;BTC;Alım;1;;1;\r\n\ 03.11.2026;USD;Alım;abc;;100;\r\n"; let dry = import(&app, &s, "asset-transactions", file, false).await; assert_eq!(dry["valid"], 3); assert_eq!( errors(&dry), vec![ (4, "Satış miktarı o tarihte eldeki miktarı aşıyor".into()), (6, "Varlık bulunamadı: BTC".into()), (7, "Miktar geçersiz: abc".into()), ] ); let r = import(&app, &s, "asset-transactions", file, true).await; assert_eq!(r["imported"], 3); let (_, assets) = app.get("/assets", &s).await; let gram = assets .as_array() .unwrap() .iter() .find(|a| a["code"] == "XAU_GRAM") .unwrap(); assert_eq!(gram["quantity_e6"], 6_000_000); assert_eq!(gram["realized_pl"], 80_000); let tl = assets .as_array() .unwrap() .iter() .find(|a| a["code"] == "TRY") .unwrap(); assert_eq!(tl["cost"], 500_000); // Dışa aktar → geri yükle: hepsi tekrar let (_, _, csv) = app.get_text("/export/asset-transactions.csv", &s).await; assert!( csv.contains("01.11.2026;XAU_GRAM;Gram Altın;Alım;10;6300;63000,00;Kuyumcu"), "{csv}" ); let r = import(&app, &s, "asset-transactions", &csv, false).await; assert_eq!( (r["duplicates"].as_u64(), r["valid"].as_u64()), (Some(3), Some(0)) ); app.cleanup().await; }