1
//! `create-transaction-logical` and `update-transaction-logical` host fns.
2
//!
3
//! Both resolve account/commodity names via `logical_common` and delegate
4
//! physical lowering to `server::logical`.
5

            
6
use chrono::Utc;
7
use server::command::transaction::{CreateTransaction, UpdateTransaction};
8
use server::command::{CmdError, CmdResult, FinanceEntity};
9
use server::logical::lower_logical_transaction;
10
use uuid::Uuid;
11

            
12
use super::logical_common::{
13
    physical_splits_to_entities, price_rows_to_entities, resolve_logical_split,
14
};
15
use super::parse::{
16
    parse_create_transaction_logical_payload, parse_update_transaction_logical_payload,
17
};
18

            
19
116
pub(super) async fn run_create_transaction_logical(
20
116
    user_id: Uuid,
21
116
    payload_arg: Option<String>,
22
116
) -> wasmtime::Result<String> {
23
    const CTX: &str = "create-transaction-logical";
24
116
    let payload = payload_arg
25
116
        .filter(|s| !s.is_empty())
26
116
        .ok_or_else(|| wasmtime::Error::msg(format!("{CTX}: missing or empty payload")))?;
27
116
    let input = parse_create_transaction_logical_payload(&payload)
28
116
        .map_err(|err| wasmtime::Error::msg(format!("{CTX}: {err}")))?;
29
116
    let mut logical = Vec::with_capacity(input.splits.len());
30
116
    for s in input.splits {
31
116
        logical.push(resolve_logical_split(CTX, user_id, s).await?);
32
    }
33
112
    let (physical_splits, price_rows) = lower_logical_transaction(&logical)
34
112
        .map_err(|err| wasmtime::Error::msg(format!("{CTX}: {err}")))?;
35
111
    let tx_id = Uuid::new_v4();
36
111
    let post_date = input.post_date.unwrap_or_else(Utc::now);
37
111
    let enter_date = Utc::now();
38
111
    let splits = physical_splits_to_entities(physical_splits, tx_id);
39
111
    let mut runner = CreateTransaction::new()
40
111
        .user_id(user_id)
41
111
        .id(tx_id)
42
111
        .post_date(post_date)
43
111
        .enter_date(enter_date)
44
111
        .splits(splits);
45
111
    if let Some(note) = input.note {
46
2
        runner = runner.note(note);
47
109
    }
48
111
    if !price_rows.is_empty() {
49
3
        runner = runner.prices(price_rows_to_entities(price_rows, post_date));
50
108
    }
51
111
    dispatch_tx_result(CTX, runner.run().await)
52
116
}
53

            
54
33
pub(super) async fn run_update_transaction_logical(
55
33
    user_id: Uuid,
56
33
    payload_arg: Option<String>,
57
33
) -> wasmtime::Result<String> {
58
    const CTX: &str = "update-transaction-logical";
59
33
    let payload = payload_arg
60
33
        .filter(|s| !s.is_empty())
61
33
        .ok_or_else(|| wasmtime::Error::msg(format!("{CTX}: missing or empty payload")))?;
62
33
    let input = parse_update_transaction_logical_payload(&payload)
63
33
        .map_err(|err| wasmtime::Error::msg(format!("{CTX}: {err}")))?;
64
32
    let mut logical = Vec::with_capacity(input.splits.len());
65
32
    for s in input.splits {
66
32
        logical.push(resolve_logical_split(CTX, user_id, s).await?);
67
    }
68
30
    let (physical_splits, price_rows) = lower_logical_transaction(&logical)
69
30
        .map_err(|err| wasmtime::Error::msg(format!("{CTX}: {err}")))?;
70
    // A split-only edit must leave the stored dates intact: forward post_date
71
    // only when :date was explicitly provided. The price rows still need a
72
    // fallback observation date when none is given.
73
30
    let price_date = input.post_date.unwrap_or_else(Utc::now);
74
30
    let splits = physical_splits_to_entities(physical_splits, input.transaction_id);
75
30
    let mut runner = UpdateTransaction::new()
76
30
        .user_id(user_id)
77
30
        .transaction_id(input.transaction_id)
78
30
        .splits(splits);
79
30
    if let Some(post_date) = input.post_date {
80
1
        runner = runner.post_date(post_date);
81
29
    }
82
30
    if let Some(note) = input.note {
83
        runner = runner.note(note);
84
30
    }
85
30
    if !price_rows.is_empty() {
86
1
        runner = runner.prices(price_rows_to_entities(price_rows, price_date));
87
29
    }
88
30
    dispatch_tx_result(CTX, runner.run().await)
89
33
}
90

            
91
141
fn dispatch_tx_result(
92
141
    ctx: &str,
93
141
    result: Result<Option<CmdResult>, CmdError>,
94
141
) -> wasmtime::Result<String> {
95
141
    match result {
96
141
        Ok(Some(CmdResult::Entity(FinanceEntity::Transaction(tx)))) => Ok(tx.id.to_string()),
97
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
98
            "{ctx}: expected Transaction entity, got {other:?}"
99
        ))),
100
        Ok(None) => Err(wasmtime::Error::msg(format!(
101
            "{ctx}: command returned no entity"
102
        ))),
103
        Err(err) => Err(wasmtime::Error::msg(format!("{ctx}: {err}"))),
104
    }
105
141
}