1
use server::command::transaction::{
2
    CreateTransaction, DeleteTransaction, GetTransaction, GetTransactionDetail, GetTransactionTag,
3
    ListTransactions, SetTransactionTag, UpdateTransaction,
4
};
5
use server::command::{CmdResult, FinanceEntity};
6
use uuid::Uuid;
7
use wasmtime::{Caller, Rooted, StructRef};
8

            
9
use super::parse::{
10
    parse_account_filter_arg, parse_create_transaction_payload, parse_transaction_id_arg,
11
    parse_update_transaction_payload,
12
};
13
use super::render::{
14
    TransactionEntry, alloc_transaction_entity, list_transaction_entries, render_full_transaction,
15
};
16
use crate::session::SessionData;
17

            
18
/// `set-transaction-tag` upsert (idempotent). Mirrors `set-split-tag`
19
/// and `set-account-tag` — i32 return = 1 on success.
20
26
pub(super) async fn run_set_transaction_tag(
21
26
    user_id: Uuid,
22
26
    id_arg: Option<String>,
23
26
    name_arg: Option<String>,
24
26
    value_arg: Option<String>,
25
26
) -> wasmtime::Result<i32> {
26
26
    let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
27
        wasmtime::Error::msg("set-transaction-tag: missing or empty :transaction-id arg")
28
    })?;
29
26
    let transaction_id = Uuid::parse_str(&raw).map_err(|err| {
30
        wasmtime::Error::msg(format!("set-transaction-tag: invalid uuid '{raw}': {err}"))
31
    })?;
32
26
    let tag_name = name_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
33
        wasmtime::Error::msg("set-transaction-tag: missing or empty :tag-name arg")
34
    })?;
35
26
    let tag_value = value_arg
36
26
        .ok_or_else(|| wasmtime::Error::msg("set-transaction-tag: missing :tag-value arg"))?;
37
26
    SetTransactionTag::new()
38
26
        .user_id(user_id)
39
26
        .transaction_id(transaction_id)
40
26
        .tag_name(tag_name)
41
26
        .tag_value(tag_value)
42
26
        .run()
43
26
        .await
44
26
        .map(|_| 1)
45
26
        .map_err(|err| wasmtime::Error::msg(format!("set-transaction-tag: {err}")))
46
26
}
47

            
48
/// `get-transaction-tag` lookup. Empty-string return on absence — same
49
/// shape as `get-split-tag`.
50
pub(super) async fn run_get_transaction_tag(
51
    user_id: Uuid,
52
    id_arg: Option<String>,
53
    name_arg: Option<String>,
54
) -> wasmtime::Result<String> {
55
    let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
56
        wasmtime::Error::msg("get-transaction-tag: missing or empty :transaction-id arg")
57
    })?;
58
    let transaction_id = Uuid::parse_str(&raw).map_err(|err| {
59
        wasmtime::Error::msg(format!("get-transaction-tag: invalid uuid '{raw}': {err}"))
60
    })?;
61
    let tag_name = name_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
62
        wasmtime::Error::msg("get-transaction-tag: missing or empty :tag-name arg")
63
    })?;
64
    match GetTransactionTag::new()
65
        .user_id(user_id)
66
        .transaction_id(transaction_id)
67
        .tag_name(tag_name)
68
        .run()
69
        .await
70
    {
71
        Ok(Some(CmdResult::String(s))) => Ok(s),
72
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
73
            "get-transaction-tag: expected String, got {other:?}"
74
        ))),
75
        Ok(None) => Ok(String::new()),
76
        Err(err) => Err(wasmtime::Error::msg(format!("get-transaction-tag: {err}"))),
77
    }
78
}
79

            
80
/// Companion to create-transaction. Payload mirrors the create shape but
81
/// `:transaction-id` is required (the row being updated) and every other
82
/// field is optional — caller only supplies what they want changed.
83
/// Splits when present REPLACE the existing set; partial split updates
84
/// aren't representable in this slice (the server's UpdateTransaction
85
/// runner treats `splits` as a full-list replace).
86
29
pub(super) async fn run_update_transaction(
87
29
    user_id: Uuid,
88
29
    payload_arg: Option<String>,
89
29
) -> wasmtime::Result<String> {
90
29
    let payload = payload_arg
91
29
        .filter(|s| !s.is_empty())
92
29
        .ok_or_else(|| wasmtime::Error::msg("update-transaction: missing or empty :payload arg"))?;
93
28
    let input = parse_update_transaction_payload(&payload)
94
28
        .map_err(|err| wasmtime::Error::msg(format!("update-transaction: {err}")))?;
95
28
    let mut runner = UpdateTransaction::new()
96
28
        .user_id(user_id)
97
28
        .transaction_id(input.transaction_id);
98
28
    if let Some(post_date) = input.post_date {
99
        runner = runner.post_date(post_date);
100
28
    }
101
28
    if let Some(enter_date) = input.enter_date {
102
        runner = runner.enter_date(enter_date);
103
28
    }
104
28
    if let Some(note) = input.note {
105
26
        runner = runner.note(note);
106
28
    }
107
28
    if let Some(splits) = input.splits {
108
2
        let entities: Vec<FinanceEntity> = splits
109
2
            .into_iter()
110
4
            .map(|mut s| {
111
4
                s.tx_id = input.transaction_id;
112
4
                FinanceEntity::Split(s)
113
4
            })
114
2
            .collect();
115
2
        runner = runner.splits(entities);
116
26
    }
117
28
    if !input.prices.is_empty() {
118
2
        let price_entities: Vec<FinanceEntity> =
119
2
            input.prices.into_iter().map(FinanceEntity::Price).collect();
120
2
        runner = runner.prices(price_entities);
121
28
    }
122
28
    match runner.run().await {
123
1
        Ok(Some(CmdResult::Entity(FinanceEntity::Transaction(tx)))) => Ok(tx.id.to_string()),
124
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
125
            "update-transaction: expected Transaction entity, got {other:?}"
126
        ))),
127
        Ok(None) => Err(wasmtime::Error::msg(
128
            "update-transaction: command returned no entity",
129
        )),
130
27
        Err(err) => Err(wasmtime::Error::msg(format!("update-transaction: {err}"))),
131
    }
132
29
}
133

            
134
730
pub(super) async fn run_create_transaction(
135
730
    user_id: Uuid,
136
730
    payload_arg: Option<String>,
137
730
) -> wasmtime::Result<String> {
138
730
    let payload = payload_arg
139
730
        .filter(|s| !s.is_empty())
140
730
        .ok_or_else(|| wasmtime::Error::msg("create-transaction: missing or empty :payload arg"))?;
141
729
    let input = parse_create_transaction_payload(&payload)
142
729
        .map_err(|err| wasmtime::Error::msg(format!("create-transaction: {err}")))?;
143
728
    let splits: Vec<FinanceEntity> = input
144
728
        .splits
145
728
        .into_iter()
146
1482
        .map(|mut s| {
147
1482
            s.tx_id = input.id;
148
1482
            FinanceEntity::Split(s)
149
1482
        })
150
728
        .collect();
151
728
    let mut runner = CreateTransaction::new()
152
728
        .user_id(user_id)
153
728
        .id(input.id)
154
728
        .post_date(input.post_date)
155
728
        .enter_date(input.enter_date)
156
728
        .splits(splits);
157
728
    if let Some(note) = input.note {
158
494
        runner = runner.note(note);
159
494
    }
160
728
    if !input.prices.is_empty() {
161
52
        let price_entities: Vec<FinanceEntity> =
162
52
            input.prices.into_iter().map(FinanceEntity::Price).collect();
163
52
        runner = runner.prices(price_entities);
164
676
    }
165
728
    match runner.run().await {
166
728
        Ok(Some(CmdResult::Entity(FinanceEntity::Transaction(tx)))) => Ok(tx.id.to_string()),
167
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
168
            "create-transaction: expected Transaction entity, got {other:?}"
169
        ))),
170
        Ok(None) => Err(wasmtime::Error::msg(
171
            "create-transaction: command returned no entity",
172
        )),
173
        Err(err) => Err(wasmtime::Error::msg(format!("create-transaction: {err}"))),
174
    }
175
730
}
176

            
177
/// Removes the transaction by UUID along with its splits, prices, and
178
/// owned tags. Not idempotent — server returns an error when the row
179
/// isn't there. Returns `t` on success.
180
54
pub(super) async fn run_delete_transaction(
181
54
    user_id: Uuid,
182
54
    id_arg: Option<String>,
183
54
) -> wasmtime::Result<i32> {
184
54
    let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
185
1
        wasmtime::Error::msg("delete-transaction: missing or empty :transaction-id arg")
186
1
    })?;
187
53
    let transaction_id = Uuid::parse_str(&raw).map_err(|err| {
188
1
        wasmtime::Error::msg(format!("delete-transaction: invalid uuid '{raw}': {err}"))
189
1
    })?;
190
52
    DeleteTransaction::new()
191
52
        .user_id(user_id)
192
52
        .transaction_id(transaction_id)
193
52
        .run()
194
52
        .await
195
52
        .map(|_| 1)
196
52
        .map_err(|err| wasmtime::Error::msg(format!("delete-transaction: {err}")))
197
54
}
198

            
199
52
pub(super) async fn run_get_transaction(
200
52
    caller: &mut Caller<'_, SessionData>,
201
52
    user_id: Uuid,
202
52
    id_arg: Option<String>,
203
52
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
204
52
    let transaction_id = parse_transaction_id_arg(id_arg)?;
205
52
    let result = GetTransaction::new()
206
52
        .user_id(user_id)
207
52
        .transaction_id(transaction_id)
208
52
        .run()
209
52
        .await;
210
52
    let entries = list_transaction_entries("get-transaction", result)?;
211
52
    match entries.into_iter().next() {
212
26
        Some((id, note, amount, post_date)) => Ok(Some(
213
26
            alloc_transaction_entity(
214
26
                caller,
215
26
                &id,
216
26
                note.as_deref(),
217
26
                amount.as_deref(),
218
26
                Some(&post_date),
219
26
            )
220
26
            .await?,
221
        )),
222
26
        None => Ok(None),
223
    }
224
52
}
225

            
226
/// `get-transaction-detail` — returns the full transaction as a string
227
/// plist (`:id :post-date :enter-date :note :splits :prices`) so a client
228
/// can reconstruct + edit it. Empty `:prices ()` when single-currency.
229
3
pub(super) async fn run_get_transaction_detail(
230
3
    user_id: Uuid,
231
3
    id_arg: Option<String>,
232
3
) -> wasmtime::Result<Option<String>> {
233
3
    let transaction_id = parse_transaction_id_arg(id_arg)?;
234
3
    let result = GetTransactionDetail::new()
235
3
        .user_id(user_id)
236
3
        .transaction_id(transaction_id)
237
3
        .run()
238
3
        .await;
239
3
    render_full_transaction("get-transaction-detail", result)
240
3
}
241

            
242
183
pub(super) async fn run_list_transactions(
243
183
    user_id: Uuid,
244
183
    account_arg: Option<String>,
245
183
) -> wasmtime::Result<Vec<TransactionEntry>> {
246
183
    let mut cmd = ListTransactions::new().user_id(user_id);
247
183
    if let Some(account_id) = parse_account_filter_arg(account_arg)? {
248
26
        cmd = cmd.account(account_id);
249
157
    }
250
183
    let result = cmd.run().await;
251
183
    list_transaction_entries("list-transactions", result)
252
183
}