bae705aa97
- 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
246 lines
7.9 KiB
Rust
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());
|
|
}
|
|
}
|