1
//! Wire-payload builder — stringifies server-side physical splits/prices to
2
//! the `(create-transaction "...")` nomiscript form.
3
//!
4
//! The FX math and logical→physical lowering live in `server::logical`.
5
//! This module owns only the plist serialisation and the public builder fns.
6

            
7
use chrono::{DateTime, Utc};
8
use server::command::CmdError;
9

            
10
pub use server::logical::{
11
    LogicalSplit, PhysicalSplit, PriceRow, lower_logical_split, lower_logical_transaction,
12
};
13

            
14
use super::{escape_str, format_rational};
15
use crate::CommandError;
16

            
17
46
fn format_split(s: &PhysicalSplit) -> String {
18
46
    format!(
19
        "(:account-id \"{}\" :commodity-id \"{}\" :id \"{}\" :value {})",
20
        s.account_id,
21
        s.commodity_id,
22
        s.id,
23
46
        format_rational(s.value)
24
    )
25
46
}
26

            
27
5
fn format_price(p: &PriceRow) -> String {
28
5
    let date_part = p
29
5
        .date
30
5
        .map_or_else(String::new, |d| format!(" :date \"{}\"", d.to_rfc3339()));
31
5
    format!(
32
        "(:commodity-id \"{}\" :currency-id \"{}\" :commodity-split \"{}\" :currency-split \"{}\" :value-num {} :value-denom {}{})",
33
        p.commodity_id,
34
        p.currency_id,
35
        p.commodity_split,
36
        p.currency_split,
37
        p.value_num,
38
        p.value_denom,
39
        date_part
40
    )
41
5
}
42

            
43
22
fn format_payload(
44
22
    post_date: &DateTime<Utc>,
45
22
    note: Option<&str>,
46
22
    splits: &[PhysicalSplit],
47
22
    prices: &[PriceRow],
48
22
) -> String {
49
22
    let date_str = post_date.to_rfc3339();
50
22
    let splits_str = splits
51
22
        .iter()
52
22
        .map(format_split)
53
22
        .collect::<Vec<_>>()
54
22
        .join(" ");
55
22
    let note_part = note.map_or_else(String::new, |n| format!(" :note {}", escape_str(n)));
56
22
    let prices_part = if prices.is_empty() {
57
17
        String::new()
58
    } else {
59
5
        let price_strs = prices
60
5
            .iter()
61
5
            .map(format_price)
62
5
            .collect::<Vec<_>>()
63
5
            .join(" ");
64
5
        format!(" :prices ({price_strs})")
65
    };
66
22
    format!(
67
        "(:post-date \"{date_str}\" :enter-date \"{date_str}\" :splits ({splits_str}){prices_part}{note_part})"
68
    )
69
22
}
70

            
71
6
fn cmd_err(e: CmdError) -> CommandError {
72
6
    match e {
73
6
        CmdError::Args(msg) => CommandError::Argument(msg),
74
        other => CommandError::Execution(other),
75
    }
76
6
}
77

            
78
/// Build `(create-transaction "...")` from a single logical split with an explicit post date.
79
17
pub fn build_create_transaction_form_dated(
80
17
    ls: &LogicalSplit,
81
17
    note: Option<&str>,
82
17
    post_date: DateTime<Utc>,
83
17
) -> Result<String, CommandError> {
84
17
    let (splits, prices) = lower_logical_transaction(std::slice::from_ref(ls)).map_err(cmd_err)?;
85
16
    let payload = format_payload(&post_date, note, &splits, &prices);
86
16
    Ok(format!("(create-transaction {})", escape_str(&payload)))
87
17
}
88

            
89
/// Build `(create-transaction "...")` from multiple logical splits in one payload.
90
11
pub fn build_create_transaction_multi(
91
11
    logical_splits: &[LogicalSplit],
92
11
    note: Option<&str>,
93
11
    post_date: DateTime<Utc>,
94
11
) -> Result<String, CommandError> {
95
11
    let (splits, prices) = lower_logical_transaction(logical_splits).map_err(cmd_err)?;
96
6
    let payload = format_payload(&post_date, note, &splits, &prices);
97
6
    Ok(format!("(create-transaction {})", escape_str(&payload)))
98
11
}