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
25
pub(super) async fn run_set_transaction_tag(
21
25
    user_id: Uuid,
22
25
    id_arg: Option<String>,
23
25
    name_arg: Option<String>,
24
25
    value_arg: Option<String>,
25
25
) -> wasmtime::Result<i32> {
26
25
    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
25
    let transaction_id = Uuid::parse_str(&raw).map_err(|err| {
30
        wasmtime::Error::msg(format!("set-transaction-tag: invalid uuid '{raw}': {err}"))
31
    })?;
32
25
    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
25
    let tag_value = value_arg
36
25
        .ok_or_else(|| wasmtime::Error::msg("set-transaction-tag: missing :tag-value arg"))?;
37
25
    SetTransactionTag::new()
38
25
        .user_id(user_id)
39
25
        .transaction_id(transaction_id)
40
25
        .tag_name(tag_name)
41
25
        .tag_value(tag_value)
42
25
        .run()
43
25
        .await
44
25
        .map(|_| 1)
45
25
        .map_err(|err| wasmtime::Error::msg(format!("set-transaction-tag: {err}")))
46
25
}
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
28
pub(super) async fn run_update_transaction(
87
28
    user_id: Uuid,
88
28
    payload_arg: Option<String>,
89
28
) -> wasmtime::Result<String> {
90
28
    let payload = payload_arg
91
28
        .filter(|s| !s.is_empty())
92
28
        .ok_or_else(|| wasmtime::Error::msg("update-transaction: missing or empty :payload arg"))?;
93
27
    let input = parse_update_transaction_payload(&payload)
94
27
        .map_err(|err| wasmtime::Error::msg(format!("update-transaction: {err}")))?;
95
27
    let mut runner = UpdateTransaction::new()
96
27
        .user_id(user_id)
97
27
        .transaction_id(input.transaction_id);
98
27
    if let Some(post_date) = input.post_date {
99
        runner = runner.post_date(post_date);
100
27
    }
101
27
    if let Some(enter_date) = input.enter_date {
102
        runner = runner.enter_date(enter_date);
103
27
    }
104
27
    if let Some(note) = input.note {
105
25
        runner = runner.note(note);
106
27
    }
107
27
    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
25
    }
117
27
    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
27
    }
122
27
    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
26
        Err(err) => Err(wasmtime::Error::msg(format!("update-transaction: {err}"))),
131
    }
132
28
}
133

            
134
702
pub(super) async fn run_create_transaction(
135
702
    user_id: Uuid,
136
702
    payload_arg: Option<String>,
137
702
) -> wasmtime::Result<String> {
138
702
    let payload = payload_arg
139
702
        .filter(|s| !s.is_empty())
140
702
        .ok_or_else(|| wasmtime::Error::msg("create-transaction: missing or empty :payload arg"))?;
141
701
    let input = parse_create_transaction_payload(&payload)
142
701
        .map_err(|err| wasmtime::Error::msg(format!("create-transaction: {err}")))?;
143
700
    let splits: Vec<FinanceEntity> = input
144
700
        .splits
145
700
        .into_iter()
146
1425
        .map(|mut s| {
147
1425
            s.tx_id = input.id;
148
1425
            FinanceEntity::Split(s)
149
1425
        })
150
700
        .collect();
151
700
    let mut runner = CreateTransaction::new()
152
700
        .user_id(user_id)
153
700
        .id(input.id)
154
700
        .post_date(input.post_date)
155
700
        .enter_date(input.enter_date)
156
700
        .splits(splits);
157
700
    if let Some(note) = input.note {
158
475
        runner = runner.note(note);
159
475
    }
160
700
    if !input.prices.is_empty() {
161
50
        let price_entities: Vec<FinanceEntity> =
162
50
            input.prices.into_iter().map(FinanceEntity::Price).collect();
163
50
        runner = runner.prices(price_entities);
164
650
    }
165
700
    match runner.run().await {
166
700
        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
702
}
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
52
pub(super) async fn run_delete_transaction(
181
52
    user_id: Uuid,
182
52
    id_arg: Option<String>,
183
52
) -> wasmtime::Result<i32> {
184
52
    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
51
    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
50
    DeleteTransaction::new()
191
50
        .user_id(user_id)
192
50
        .transaction_id(transaction_id)
193
50
        .run()
194
50
        .await
195
50
        .map(|_| 1)
196
50
        .map_err(|err| wasmtime::Error::msg(format!("delete-transaction: {err}")))
197
52
}
198

            
199
50
pub(super) async fn run_get_transaction(
200
50
    caller: &mut Caller<'_, SessionData>,
201
50
    user_id: Uuid,
202
50
    id_arg: Option<String>,
203
50
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
204
50
    let transaction_id = parse_transaction_id_arg(id_arg)?;
205
50
    let result = GetTransaction::new()
206
50
        .user_id(user_id)
207
50
        .transaction_id(transaction_id)
208
50
        .run()
209
50
        .await;
210
50
    let entries = list_transaction_entries("get-transaction", result)?;
211
50
    match entries.into_iter().next() {
212
25
        Some((id, note, amount, post_date)) => Ok(Some(
213
25
            alloc_transaction_entity(
214
25
                caller,
215
25
                &id,
216
25
                note.as_deref(),
217
25
                amount.as_deref(),
218
25
                Some(&post_date),
219
25
            )
220
25
            .await?,
221
        )),
222
25
        None => Ok(None),
223
    }
224
50
}
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
176
pub(super) async fn run_list_transactions(
243
176
    user_id: Uuid,
244
176
    account_arg: Option<String>,
245
176
) -> wasmtime::Result<Vec<TransactionEntry>> {
246
176
    let mut cmd = ListTransactions::new().user_id(user_id);
247
176
    if let Some(account_id) = parse_account_filter_arg(account_arg)? {
248
25
        cmd = cmd.account(account_id);
249
151
    }
250
176
    let result = cmd.run().await;
251
176
    list_transaction_entries("list-transactions", result)
252
176
}