66a406c05b42866662552f9bb6ade65b0200b6a6
[blender-dev-fund.git] / looper / tests / test_money.py
1 from unittest import TestCase
2
3 import django.test
4
5 from looper.money import *
6
7 EUR: typing.Callable[[int], Money] = functools.partial(Money, 'EUR')
8 USD: typing.Callable[[int], Money] = functools.partial(Money, 'USD')
9
10
11 class BasicOperationsTest(TestCase):
12     def test_properties(self) -> None:
13         self.assertEqual('EUR', EUR(414).currency)
14         self.assertEqual(414, EUR(414).cents)
15
16     def test_bad_currency(self) -> None:
17         # Unfortunately, to support the MoneyField on new objects, we
18         # have to support currency-less Money instances.
19         self.assertEqual(55, Money('', 55).cents)
20
21         with self.assertRaises(TypeError):
22             Money(44, 55)  # type: ignore
23         with self.assertRaises(TypeError):
24             Money(None, 55)  # type: ignore
25
26     def test_bad_cents(self) -> None:
27         with self.assertRaises(TypeError):
28             Money('EUR', 44.3)  # type: ignore
29         with self.assertRaises(TypeError):
30             Money('EUR', 44j)  # type: ignore
31
32     def test_str(self) -> None:
33         self.assertEqual('EUR\u00A05.00', str(EUR(500)))
34         self.assertEqual('USD\u00A00.01', str(Money('USD', 1)))
35
36     def test_repr(self) -> None:
37         self.assertEqual("Money(currency='EUR', cents=12344)", repr(EUR(12344)))
38
39     def test_arithmetic_happy(self) -> None:
40         self.assertEqual(EUR(-4315), +EUR(-4315))
41         self.assertEqual(EUR(-4315), -EUR(4315))
42         self.assertEqual(EUR(4315), EUR(315) + EUR(4000))
43         self.assertEqual(EUR(-3685), EUR(315) - EUR(4000))
44         self.assertEqual(EUR(15), 3 * EUR(5))
45         self.assertEqual(EUR(15), EUR(5) * 3)
46
47     def test_ordering(self) -> None:
48         self.assertLess(EUR(15), EUR(16))
49         self.assertLessEqual(EUR(15), EUR(16))
50         self.assertLessEqual(EUR(15), EUR(15))
51         self.assertGreater(EUR(16), EUR(15))
52         self.assertGreaterEqual(EUR(16), EUR(15))
53         self.assertGreaterEqual(EUR(15), EUR(15))
54         self.assertGreaterEqual(EUR(15), -EUR(100))
55
56         with self.assertRaises(TypeError):
57             EUR(15) < 30
58
59     def test_arithmetic_unsupported(self):
60         m = EUR(56123)
61         with self.assertRaises(TypeError):
62             m + 3
63         with self.assertRaises(TypeError):
64             3 + m
65         with self.assertRaises(TypeError):
66             m - 3
67         with self.assertRaises(TypeError):
68             3 - m
69         with self.assertRaises(TypeError):
70             m * m
71         with self.assertRaises(TypeError):
72             3.0 * m
73         with self.assertRaises(TypeError):
74             m * 3.0
75
76     def test_immutability(self) -> None:
77         m1 = EUR(4123)
78         m2 = m1
79         m1 += EUR(1)
80         self.assertEqual(EUR(4124), m1)
81         self.assertEqual(EUR(4123), m2)
82
83     def test_true_division(self) -> None:
84         self.assertEqual([EUR(4), EUR(4), EUR(3), EUR(3)], EUR(14) / 4)
85
86         with self.assertRaises(TypeError):
87             EUR(14) / 4.0  # type: ignore
88         with self.assertRaises(ValueError):
89             EUR(14) / -4
90         with self.assertRaises(ZeroDivisionError):
91             EUR(14) / 0
92
93     def test_floor_division(self) -> None:
94         self.assertEqual(EUR(3), EUR(14) // 4)
95
96         self.assertEqual(EUR(3), EUR(14) // 4.0)
97         self.assertEqual(EUR(3), EUR(14) // 4.6)
98         self.assertEqual(EUR(2), EUR(14) // 4.7)
99
100         with self.assertRaises(TypeError):
101             EUR(14) // 4.0j  # type: ignore
102         with self.assertRaises(TypeError):
103             EUR(14) // EUR(4)  # type: ignore
104         with self.assertRaises(ValueError):
105             EUR(14) // -4
106         with self.assertRaises(ZeroDivisionError):
107             EUR(14) // 0
108
109     def test_ratio(self):
110         self.assertEqual(0.0, EUR(0) / EUR(4))
111         self.assertEqual(1.0, EUR(4) / EUR(4))
112         self.assertEqual(-2.0, EUR(4) / EUR(-2))
113         self.assertAlmostEqual(1.15, USD(17332) / USD(15072), places=3)
114
115         with self.assertRaises(CurrencyMismatch):
116             EUR(14) / USD(14)
117
118     def test_equality(self) -> None:
119         self.assertEqual(hash(('EUR', 513)), hash(EUR(513)))
120         m1 = EUR(551)
121         m2 = EUR(551)
122         self.assertIsNot(m1, m2)
123         self.assertEqual(m1, m2)
124         self.assertLess(m1, EUR(1000))
125         self.assertNotEqual(m1, 'EUR\u00A0551')
126
127     def test_bool(self) -> None:
128         self.assertTrue(EUR(1))
129         self.assertTrue(EUR(-1))
130         self.assertFalse(EUR(0))
131
132     def test_currency_mismatch(self) -> None:
133         with self.assertRaises(CurrencyMismatch):
134             EUR(14) + USD(44)
135         with self.assertRaises(CurrencyMismatch):
136             EUR(14) - USD(44)
137
138         with self.assertRaises(CurrencyMismatch):
139             EUR(14) < USD(14)
140
141     def test_as_decimal_string(self) -> None:
142         m1 = EUR(1000)
143         self.assertEqual(m1.decimals_string, '10.00')
144         m2 = EUR(9999)
145         self.assertEqual(m2.decimals_string, '99.99')
146         m2 = EUR(1010)
147         self.assertEqual(m2.decimals_string, '10.10')
148         m2 = EUR(1009)
149         self.assertEqual(m2.decimals_string, '10.09')
150         m2 = EUR(1033)
151         self.assertEqual(m2.decimals_string, '10.33')
152
153     def test_with_currency_symbol(self) -> None:
154         self.assertEqual('€\u00A010.00', EUR(1000).with_currency_symbol())
155         self.assertEqual('$\u00A010.00', USD(1000).with_currency_symbol())
156         self.assertEqual('¥\u00A010.00', Money('JPY', 1000).with_currency_symbol())
157
158     def test_with_currency_symbol_nonocents(self) -> None:
159         self.assertEqual('€\u00A010', EUR(1000).with_currency_symbol_nonocents())
160         self.assertEqual('$\u00A010.50', USD(1050).with_currency_symbol_nonocents())
161         self.assertEqual('¥\u00A00.99', Money('JPY', 99).with_currency_symbol_nonocents())
162
163
164 class SumTest(TestCase):
165     def test_empty(self) -> None:
166         self.assertEqual({}, sum_per_currency([]))
167
168     def test_one_currency_one_amount(self) -> None:
169         self.assertEqual({'HRK': Money('HRK', 47)}, sum_per_currency([Money('HRK', 47)]))
170
171     def test_one_currency_multiple_amounts(self) -> None:
172         self.assertEqual({'HRK': Money('HRK', 374)},
173                          sum_per_currency([Money('HRK', 47), Money('HRK', 327)]))
174
175     def test_multiple_currencies(self) -> None:
176         self.assertEqual(
177             {'HRK': Money('HRK', 47 + 327),
178              'SGD': Money('SGD', 413 + 385)},
179             sum_per_currency([
180                 Money('HRK', 47),
181                 Money('HRK', 327),
182                 Money('SGD', 413),
183                 Money('SGD', 385),
184             ])
185         )
186
187         self.assertEqual(
188             {'HRK': Money('HRK', 47 + 327),
189              'SGD': Money('SGD', 413 + 385),
190              'USD': Money('USD', 161)},
191             sum_per_currency([
192                 Money('HRK', 327),
193                 Money('SGD', 413),
194                 Money('HRK', 47),
195                 Money('USD', 161),
196                 Money('SGD', 385),
197             ])
198         )
199
200
201 @django.test.override_settings(
202     LOOPER_CONVERTION_RATES_FROM_EURO={
203         'EUR': 1.0,
204         'USD': 1.15,  # US$ 1.15 = € 1.00
205     }
206 )
207 class SumToEuroTest(django.test.TestCase):
208     def test_empty(self) -> None:
209         self.assertEqual(Money('EUR', 0), sum_to_euros([]))
210
211     def test_single_euro(self) -> None:
212         self.assertEqual(Money('EUR', 215), sum_to_euros([Money('EUR', 215)]))
213
214     def test_multiple_euro(self) -> None:
215         self.assertEqual(Money('EUR', 315), sum_to_euros([Money('EUR', 215), Money('EUR', 100)]))
216
217     def test_multiple_currencies(self) -> None:
218         self.assertEqual(Money('EUR', 287), sum_to_euros([Money('EUR', 200), Money('USD', 100)]))
219
220
221 @django.test.override_settings(
222     LOOPER_CONVERTION_RATES_FROM_EURO={
223         'EUR': 1.0,
224         'USD': 1.15,  # US$ 1.15 = € 1.00
225         'SGD': 1.59,  # SG$ 1.59 = € 1.00
226     }
227 )
228 class ConvertCurrencyTest(django.test.TestCase):
229     def test_identical(self) -> None:
230         amt = Money('EUR', 47)
231         self.assertIs(amt, convert_currency(amt, to_currency='EUR'))
232
233     def test_usd_to_eur(self) -> None:
234         self.assertEqual(Money('EUR', 435), convert_currency(Money('USD', 500), to_currency='EUR'))
235
236     def test_eur_to_usd(self) -> None:
237         self.assertEqual(Money('USD', 500), convert_currency(Money('EUR', 435), to_currency='USD'))
238
239     def test_sgd_to_usd(self) -> None:
240         self.assertEqual(Money('USD', 34230),
241                          convert_currency(Money('SGD', 47327), to_currency='USD'))
242
243
244 class PickleMoneyTest(TestCase):
245     def test_pickle(self) -> None:
246         import pickle
247
248         amt = Money('HRK', 716)
249         pickled = pickle.dumps(amt)
250         unpickled = pickle.loads(pickled)
251
252         self.assertEqual(amt, unpickled)