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

            
54
32
pub(super) async fn run_update_transaction_logical(
55
32
    user_id: Uuid,
56
32
    payload_arg: Option<String>,
57
32
) -> wasmtime::Result<String> {
58
    const CTX: &str = "update-transaction-logical";
59
32
    let payload = payload_arg
60
32
        .filter(|s| !s.is_empty())
61
32
        .ok_or_else(|| wasmtime::Error::msg(format!("{CTX}: missing or empty payload")))?;
62
32
    let input = parse_update_transaction_logical_payload(&payload)
63
32
        .map_err(|err| wasmtime::Error::msg(format!("{CTX}: {err}")))?;
64
31
    let mut logical = Vec::with_capacity(input.splits.len());
65
31
    for s in input.splits {
66
31
        logical.push(resolve_logical_split(CTX, user_id, s).await?);
67
    }
68
29
    let (physical_splits, price_rows) = lower_logical_transaction(&logical)
69
29
        .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
29
    let price_date = input.post_date.unwrap_or_else(Utc::now);
74
29
    let splits = physical_splits_to_entities(physical_splits, input.transaction_id);
75
29
    let mut runner = UpdateTransaction::new()
76
29
        .user_id(user_id)
77
29
        .transaction_id(input.transaction_id)
78
29
        .splits(splits);
79
29
    if let Some(post_date) = input.post_date {
80
1
        runner = runner.post_date(post_date);
81
28
    }
82
29
    if let Some(note) = input.note {
83
        runner = runner.note(note);
84
29
    }
85
29
    if !price_rows.is_empty() {
86
1
        runner = runner.prices(price_rows_to_entities(price_rows, price_date));
87
28
    }
88
29
    dispatch_tx_result(CTX, runner.run().await)
89
32
}
90

            
91
136
fn dispatch_tx_result(
92
136
    ctx: &str,
93
136
    result: Result<Option<CmdResult>, CmdError>,
94
136
) -> wasmtime::Result<String> {
95
136
    match result {
96
136
        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
136
}