1
//! Tests for the logical→physical transaction lowering.
2

            
3
use num_rational::Rational64;
4
use sqlx::types::Uuid;
5

            
6
use crate::command::CmdError;
7

            
8
use super::{LogicalSplit, PriceRow, lower_logical_split, lower_logical_transaction};
9

            
10
3
fn same_currency_ls() -> LogicalSplit {
11
3
    let c = Uuid::new_v4();
12
3
    LogicalSplit {
13
3
        from: Uuid::new_v4(),
14
3
        to: Uuid::new_v4(),
15
3
        from_commodity: c,
16
3
        to_commodity: c,
17
3
        value: Rational64::new(100, 1),
18
3
        to_amount: None,
19
3
    }
20
3
}
21

            
22
8
fn cross_currency_ls(value: Rational64, to_amount: Rational64) -> LogicalSplit {
23
8
    LogicalSplit {
24
8
        from: Uuid::new_v4(),
25
8
        to: Uuid::new_v4(),
26
8
        from_commodity: Uuid::new_v4(),
27
8
        to_commodity: Uuid::new_v4(),
28
8
        value,
29
8
        to_amount: Some(to_amount),
30
8
    }
31
8
}
32

            
33
#[test]
34
1
fn single_currency_produces_no_price() {
35
1
    let ls = same_currency_ls();
36
1
    let (from, to, price) = lower_logical_split(&ls).expect("same-currency lowering");
37
1
    assert!(
38
1
        price.is_none(),
39
        "same-currency must not produce a price row"
40
    );
41
1
    assert_eq!(from.account_id, ls.from);
42
1
    assert_eq!(to.account_id, ls.to);
43
1
    assert_eq!(from.value, -ls.value);
44
1
    assert_eq!(to.value, ls.value);
45
1
}
46

            
47
#[test]
48
1
fn cross_currency_price_num_denom_and_split_linkage() {
49
1
    let value = Rational64::new(100, 1);
50
1
    let to_amount = Rational64::new(15000, 1);
51
1
    let ls = cross_currency_ls(value, to_amount);
52
1
    let (from, to, price) = lower_logical_split(&ls).expect("cross-currency lowering");
53
    let PriceRow {
54
1
        commodity_id,
55
1
        currency_id,
56
1
        commodity_split,
57
1
        currency_split,
58
1
        value_num,
59
1
        value_denom,
60
        ..
61
1
    } = price.expect("cross-currency must produce a price row");
62
    // 100/15000 reduces to 1/150
63
1
    assert_eq!(value_num, 1);
64
1
    assert_eq!(value_denom, 150);
65
1
    assert_eq!(commodity_id, ls.to_commodity);
66
1
    assert_eq!(currency_id, ls.from_commodity);
67
    // to_id is the commodity-split, from_id is the currency-split
68
1
    assert_eq!(commodity_split, to.id);
69
1
    assert_eq!(currency_split, from.id);
70
1
}
71

            
72
#[test]
73
1
fn zero_value_is_error() {
74
1
    let ls = cross_currency_ls(Rational64::new(0, 1), Rational64::new(100, 1));
75
1
    let err = lower_logical_split(&ls).expect_err("zero value must error");
76
1
    assert!(
77
1
        matches!(err, CmdError::Args(ref m) if m.contains("value must be positive")),
78
        "expected positive-value error, got: {err:?}"
79
    );
80
1
}
81

            
82
#[test]
83
1
fn negative_value_is_error() {
84
1
    let ls = cross_currency_ls(Rational64::new(-100, 1), Rational64::new(50, 1));
85
1
    let err = lower_logical_split(&ls).expect_err("negative value must error");
86
1
    assert!(
87
1
        matches!(err, CmdError::Args(ref m) if m.contains("value must be positive")),
88
        "expected positive-value error, got: {err:?}"
89
    );
90
1
}
91

            
92
#[test]
93
1
fn zero_to_amount_is_error() {
94
1
    let ls = cross_currency_ls(Rational64::new(100, 1), Rational64::new(0, 1));
95
1
    let err = lower_logical_split(&ls).expect_err("zero to_amount must error");
96
1
    assert!(
97
1
        matches!(err, CmdError::Args(ref m) if m.contains("to_amount must be positive")),
98
        "expected positive-to_amount error, got: {err:?}"
99
    );
100
1
}
101

            
102
#[test]
103
1
fn negative_to_amount_is_error() {
104
1
    let ls = cross_currency_ls(Rational64::new(100, 1), Rational64::new(-50, 1));
105
1
    let err = lower_logical_split(&ls).expect_err("negative to_amount must error");
106
1
    assert!(
107
1
        matches!(err, CmdError::Args(ref m) if m.contains("to_amount must be positive")),
108
        "expected positive-to_amount error, got: {err:?}"
109
    );
110
1
}
111

            
112
#[test]
113
1
fn same_currency_with_to_amount_is_error() {
114
1
    let mut ls = same_currency_ls();
115
1
    ls.to_amount = Some(Rational64::new(42, 1));
116
1
    let err = lower_logical_split(&ls).expect_err("same-currency to_amount must error");
117
1
    assert!(
118
1
        matches!(err, CmdError::Args(ref m) if m.contains("only valid for cross-currency")),
119
        "expected cross-currency-only error, got: {err:?}"
120
    );
121
1
}
122

            
123
#[test]
124
1
fn missing_to_amount_cross_currency_is_error() {
125
1
    let ls = LogicalSplit {
126
1
        from: Uuid::new_v4(),
127
1
        to: Uuid::new_v4(),
128
1
        from_commodity: Uuid::new_v4(),
129
1
        to_commodity: Uuid::new_v4(),
130
1
        value: Rational64::new(100, 1),
131
1
        to_amount: None,
132
1
    };
133
1
    let err = lower_logical_split(&ls).expect_err("missing to_amount must error");
134
1
    assert!(
135
1
        matches!(err, CmdError::Args(ref m) if m.contains("to_amount")),
136
        "expected to_amount error, got: {err:?}"
137
    );
138
1
}
139

            
140
#[test]
141
1
fn i64_overflow_after_reduction_is_error() {
142
    // 4 * i64::MAX cannot fit i64 after reduction
143
1
    let ls = cross_currency_ls(Rational64::new(i64::MAX, 1), Rational64::new(1, 4));
144
1
    let err = lower_logical_split(&ls).expect_err("overflow must error");
145
1
    assert!(
146
1
        matches!(err, CmdError::Args(ref m) if m.contains("overflow")),
147
        "expected overflow error, got: {err:?}"
148
    );
149
1
}
150

            
151
#[test]
152
1
fn reducible_large_rate_canonicalises() {
153
    // MAX / (MAX/2) = 2/1 after reduction
154
1
    let ls = cross_currency_ls(Rational64::new(i64::MAX, 1), Rational64::new(i64::MAX, 2));
155
1
    let (_, _, price) = lower_logical_split(&ls).expect("reducible large input");
156
1
    let p = price.expect("cross-currency must produce price");
157
1
    assert_eq!(p.value_num, 2);
158
1
    assert_eq!(p.value_denom, 1);
159
1
}
160

            
161
#[test]
162
1
fn lower_logical_transaction_single_currency() {
163
1
    let ls = same_currency_ls();
164
1
    let (splits, prices) = lower_logical_transaction(&[ls]).expect("single-currency multi");
165
1
    assert_eq!(splits.len(), 2);
166
1
    assert!(prices.is_empty());
167
1
}
168

            
169
#[test]
170
1
fn lower_logical_transaction_cross_currency() {
171
1
    let ls = cross_currency_ls(Rational64::new(100, 1), Rational64::new(15000, 1));
172
1
    let (splits, prices) = lower_logical_transaction(&[ls]).expect("cross-currency multi");
173
1
    assert_eq!(splits.len(), 2);
174
1
    assert_eq!(prices.len(), 1);
175
1
}
176

            
177
#[test]
178
1
fn lower_logical_transaction_empty_is_error() {
179
1
    let err = lower_logical_transaction(&[]).expect_err("empty slice must error");
180
1
    assert!(
181
1
        matches!(err, CmdError::Args(ref m) if m.contains("at least one")),
182
        "expected at-least-one error, got: {err:?}"
183
    );
184
1
}