Files
boc/aamos-ledger-rust/crates/aamos-reports/src/transaction_report.rs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

246 lines
7.9 KiB
Rust

use crate::{TransactionEntry, TransactionLine, TransactionReport};
use aamos_core::{LedgerResult, TenantId};
use aamos_ledger::{JournalEntry, JournalLine};
use chrono::NaiveDate;
/// Genererar en transaktionsrapport för ett datumintervall.
///
/// Filtrerar verifikationer efter datum och inkluderar alla rader
/// med kontoinformation.
pub fn generate_transaction_report(
tenant_id: TenantId,
from_date: NaiveDate,
to_date: NaiveDate,
entries: &[JournalEntry],
account_names: &[(String, String)], // (account_number, account_name)
) -> LedgerResult<TransactionReport> {
let name_map: std::collections::HashMap<String, String> = account_names
.iter()
.map(|(n, name)| (n.clone(), name.clone()))
.collect();
let filtered_entries: Vec<TransactionEntry> = entries
.iter()
.filter(|e| e.entry_date >= from_date && e.entry_date <= to_date)
.map(|e| TransactionEntry {
entry_number: e.entry_number,
entry_date: e.entry_date,
description: e.description.clone(),
reference: e.reference.clone(),
lines: e
.lines
.iter()
.map(|line| map_journal_line(line, &name_map))
.collect(),
})
.collect();
Ok(TransactionReport {
tenant_id,
from_date,
to_date,
entries: filtered_entries,
})
}
fn map_journal_line(line: &JournalLine, name_map: &std::collections::HashMap<String, String>) -> TransactionLine {
TransactionLine {
account_number: line.account_number.clone(),
account_name: name_map
.get(&line.account_number)
.cloned()
.or_else(|| line.account_name.clone())
.unwrap_or_default(),
debit: line.debit,
credit: line.credit,
}
}
#[cfg(test)]
mod tests {
use super::*;
use aamos_core::{Currency, DecisionSource, Money, Period};
use aamos_ledger::{EntryStatus, SourceType};
use chrono::{Datelike, Utc};
use serde_json;
use uuid::Uuid;
fn test_tenant() -> TenantId {
TenantId::new("test-tenant")
}
fn make_journal_entry(
entry_date: NaiveDate,
entry_number: i64,
lines: Vec<JournalLine>,
) -> JournalEntry {
JournalEntry {
id: Uuid::new_v4(),
tenant_id: test_tenant(),
entry_number: Some(entry_number),
fiscal_year: entry_date.year(),
period: Period::new(entry_date.year(), entry_date.month() as u8).unwrap(),
entry_date,
description: format!("Entry {}", entry_number),
reference: Some(format!("REF{}", entry_number)),
source_type: SourceType::Manual,
source_id: None,
status: EntryStatus::Posted,
void_reason: None,
voided_at: None,
voided_by: None,
trace_id: Uuid::new_v4(),
correlation_id: Uuid::new_v4(),
user_id: "test-user".to_string(),
decision_source: DecisionSource::User,
metadata: serde_json::Value::Object(serde_json::Map::new()),
created_at: Utc::now(),
posted_at: Some(Utc::now()),
lines,
}
}
fn make_journal_line(account_number: &str, debit: Option<i64>, credit: Option<i64>) -> JournalLine {
JournalLine {
id: Uuid::new_v4(),
entry_id: Uuid::new_v4(),
tenant_id: test_tenant(),
line_number: 1,
account_number: account_number.to_string(),
account_name: None,
debit: debit.map(Money),
credit: credit.map(Money),
currency: Currency::SEK,
amount_base: None,
vat_code: None,
vat_amount: None,
cost_center: None,
project_code: None,
description: None,
metadata: serde_json::Value::Object(serde_json::Map::new()),
}
}
#[test]
fn test_transaction_report_filter_by_date() {
let entries = vec![
make_journal_entry(
NaiveDate::from_ymd_opt(2026, 1, 10).unwrap(),
1,
vec![
make_journal_line("1910", Some(10000), None),
make_journal_line("3001", None, Some(10000)),
],
),
make_journal_entry(
NaiveDate::from_ymd_opt(2026, 2, 15).unwrap(),
2,
vec![
make_journal_line("7010", Some(5000), None),
make_journal_line("1910", None, Some(5000)),
],
),
make_journal_entry(
NaiveDate::from_ymd_opt(2026, 3, 20).unwrap(),
3,
vec![
make_journal_line("6991", Some(2000), None),
make_journal_line("1910", None, Some(2000)),
],
),
];
let account_names = vec![
("1910".to_string(), "Kassa".to_string()),
("3001".to_string(), "Konsultintäkter".to_string()),
("7010".to_string(), "Lönekostnader".to_string()),
("6991".to_string(), "Kontorshyra".to_string()),
];
let report = generate_transaction_report(
test_tenant(),
NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2026, 2, 28).unwrap(),
&entries,
&account_names,
)
.unwrap();
assert_eq!(report.entries.len(), 2);
assert_eq!(report.entries[0].entry_number, Some(1));
assert_eq!(report.entries[1].entry_number, Some(2));
}
#[test]
fn test_transaction_report_account_names() {
let entries = vec![make_journal_entry(
NaiveDate::from_ymd_opt(2026, 1, 10).unwrap(),
1,
vec![
make_journal_line("1910", Some(10000), None),
make_journal_line("3001", None, Some(10000)),
],
)];
let account_names = vec![
("1910".to_string(), "Kassa".to_string()),
("3001".to_string(), "Konsultintäkter".to_string()),
];
let report = generate_transaction_report(
test_tenant(),
NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2026, 12, 31).unwrap(),
&entries,
&account_names,
)
.unwrap();
assert_eq!(report.entries[0].lines[0].account_name, "Kassa");
assert_eq!(report.entries[0].lines[1].account_name, "Konsultintäkter");
}
#[test]
fn test_transaction_report_fallback_to_line_name() {
let mut entry = make_journal_entry(
NaiveDate::from_ymd_opt(2026, 1, 10).unwrap(),
1,
vec![make_journal_line("1910", Some(10000), None)],
);
entry.lines[0].account_name = Some("Kassa (fallback)".to_string());
let account_names: Vec<(String, String)> = vec![];
let report = generate_transaction_report(
test_tenant(),
NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2026, 12, 31).unwrap(),
&[entry],
&account_names,
)
.unwrap();
assert_eq!(report.entries[0].lines[0].account_name, "Kassa (fallback)");
}
#[test]
fn test_transaction_report_empty_result() {
let entries = vec![make_journal_entry(
NaiveDate::from_ymd_opt(2026, 6, 15).unwrap(),
1,
vec![make_journal_line("1910", Some(10000), None)],
)];
let report = generate_transaction_report(
test_tenant(),
NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2026, 3, 31).unwrap(),
&entries,
&[],
)
.unwrap();
assert!(report.entries.is_empty());
}
}