//! The real audit writer (SPEC ยง14.4.7, FR-12) through the recurring generation and through sync: source, actor, //! times and append-only entries. use serde_json::json; use crate::common::*; use crate::sync_fixtures::*; #[tokio::test] async fn generation_writes_recurring_audit_with_server_now_and_no_device() { let app = app().await; app.set_now("2026-10-17T12:00:00.000Z"); let template = new_id(); sqlx::query( "INSERT INTO recurring_templates (id, account_id, category_id, amount_ore, description, notes, payer_user_id, \ split_mode, day_of_month, start_period, created_by_user_id, updated_by_user_id, created_at, updated_at, \ server_version) VALUES (?, ?, ?, 1250000, 'Hyra', '', ?, 'default', 1, '2026-10', ?, ?, ?, ?, 900000)", ) .bind(&template) .bind(ACCOUNT_1) .bind(HOME) .bind(ADMIN_ID) .bind(MEMBER_ID) .bind(MEMBER_ID) .bind(NOW) .bind(NOW) .execute(&app.state.db) .await .unwrap(); let generated = kvitt_server::api::recurring::generate_due_periods(&app.state, template.parse().unwrap()) .await .unwrap(); assert_eq!(generated.len(), 1); let audit = audit_of(&app, &generated[0].expense_id.to_string()).await; assert_eq!(audit.len(), 1); let a = &audit[0]; assert_eq!( (a.action.as_str(), a.source.as_str()), ("create", "recurring") ); assert_eq!(a.actor_user_id, MEMBER_ID); assert_eq!(a.actor_device_id, None); assert_eq!((a.client_at.as_str(), a.server_at.as_str()), (NOW, NOW)); assert_eq!( a.changes[3], json!({"field": "amount_ore", "old": null, "new": 1_250_000}) ); // The entry reaches the phones through pull. let page = pull(&app, &admin_token(), 0, None).await.json; assert!( page["audit_entries"] .as_array() .unwrap() .iter() .any(|e| e["source"] == "recurring") ); } #[tokio::test] async fn entries_are_append_only_create_update_delete() { let app = app().await; let id = new_id(); push_one( &app, &admin_token(), exp(expense(&id, "2026-10-17T10:00:00.000Z")), ) .await; let first = audit_of(&app, &id).await; let mut e = expense(&id, "2026-10-17T11:00:00.000Z"); e["description"] = json!("Coop"); push_one(&app, &member_token(), exp(e)).await; let mut d = expense(&id, "2026-10-17T11:30:00.000Z"); d["deleted_at"] = json!("2026-10-17T11:30:00.000Z"); push_one(&app, &admin_token(), exp(d)).await; let all = audit_of(&app, &id).await; let actions: Vec<_> = all.iter().map(|a| a.action.as_str()).collect(); assert_eq!(actions, ["create", "update", "delete"]); assert_eq!(all[0], first[0], "earlier entries are never updated"); assert_eq!( all[1].changes, json!([{"field": "description", "old": "ICA Maxi", "new": "Coop"}]) ); assert_eq!(all[1].actor_user_id, MEMBER_ID); assert!( all.windows(2) .all(|w| w[0].server_version < w[1].server_version) ); }