source: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py @ 12790

Last change on this file since 12790 was 12790, checked in by uli, 10 years ago

Provide proper repr() for PaymentItems?

  • Property svn:keywords set to Id
File size: 15.0 KB
Line 
1# -*- coding: utf-8 -*-
2## $Id: test_payment.py 12790 2015-03-18 14:08:18Z uli $
3##
4## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
5## This program is free software; you can redistribute it and/or modify
6## it under the terms of the GNU General Public License as published by
7## the Free Software Foundation; either version 2 of the License, or
8## (at your option) any later version.
9##
10## This program is distributed in the hope that it will be useful,
11## but WITHOUT ANY WARRANTY; without even the implied warranty of
12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13## GNU General Public License for more details.
14##
15## You should have received a copy of the GNU General Public License
16## along with this program; if not, write to the Free Software
17## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18##
19import datetime
20import decimal
21import re
22import unittest
23from zope.component import (
24    getUtilitiesFor, getSiteManager, queryUtility, getGlobalSiteManager,
25    )
26from zope.component.hooks import setSite
27from zope.interface import implements, implementer
28from zope.interface.verify import verifyClass, verifyObject
29from waeup.ikoba.payments.interfaces import (
30    IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
31    IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
32    IPayableFinder, IPayerFinder, IPayable, IPayer,
33    )
34from waeup.ikoba.app import Company
35from waeup.ikoba.payments.payment import (
36    Payment, get_payment_providers, PaymentItem, format_payment_item_values,
37    get_payment, get_payments_from_payer_id, find_payable_from_payable_id,
38    find_payer_from_payer_id, get_payments_from_payable_id, format_amount,
39    )
40from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
41
42
43@implementer(IPayer)
44class FakePayer(object):
45
46    def __init__(
47        self, payer_id=u'PAYER_01', first_name=u'Anna', last_name='Tester'):
48        self.payer_id = payer_id
49        self.first_name = first_name
50        self.last_name = last_name
51
52
53FAKE_PAYMENT_ITEMS = (
54    PaymentItem(u'ITEM1', u'Item title 1', decimal.Decimal("1.00")),
55    PaymentItem(u'ITEM2', u'Item title 2', decimal.Decimal("2.2")),
56    )
57
58
59@implementer(IPayable)
60class FakePayable(object):
61
62    payable_id = u'id1'
63
64    def __init__(self, payable_id=u'PAYABLE_01', title=u'title',
65                 currency=u'USD', payment_items=FAKE_PAYMENT_ITEMS):
66        self.payable_id = payable_id
67        self.title = title
68        self.currency = currency
69        self.payment_items = payment_items
70
71
72class HelperTests(unittest.TestCase):
73
74    def tearDown(self):
75        # unregister any IPaymentGatewayServices
76        sm = getSiteManager(None)
77        for name, util in getUtilitiesFor(IPaymentGatewayService):
78            sm.unregisterUtility(util, name=name)
79
80    def test_get_payment_providers_no_providers(self):
81        # we can get a dict of all payment providers
82        result = get_payment_providers()
83        assert isinstance(result, dict)
84        assert result == {}
85
86    def test_get_payment_providers(self):
87        # we get any payment providers registered
88        sm = getSiteManager(None)
89
90        class FakeUtil(object):
91            implements(IPaymentGatewayService)
92
93        fake_util = FakeUtil()
94        sm.registerUtility(fake_util, name=u'some_name')
95        result = get_payment_providers()
96        assert isinstance(result, dict)
97        assert result.keys() == ['some_name', ]
98        assert result['some_name'] is fake_util
99
100    def test_format_payment_item_values(self):
101        # we can format lists of payment item values
102        result = format_payment_item_values(
103            [(u'Item 1', 'USD', decimal.Decimal("12.123")),
104             (u'Item 2', 'USD', decimal.Decimal("12.002")),
105             ], 'USD')
106        self.assertEqual(
107            result, [(u'Item 1', 'USD 12.12'),
108                     (u'Item 2', 'USD 12.00'),
109                     (u'Total', 'USD 24.12')]
110            )
111
112    def test_format_payment_item_values_req_single_currency(self):
113        # we require one currency for all items, yet.
114        self.assertRaises(
115            ValueError, format_payment_item_values,
116            [(u'Item 1', 'USD', decimal.Decimal("12.12")),
117             (u'Item 2', 'EUR', decimal.Decimal("50")),
118             ],
119            'USD')
120
121    def test_format_amount(self):
122        # we can make amounts readable
123        D = decimal.Decimal
124        self.assertEqual(format_amount(D("0"), 'USD'), u"US$ 0.00")
125        self.assertEqual(format_amount(D("0.1"), 'EUR'), u"€ 0.10")
126        self.assertEqual(format_amount(D("-1.2"), 'NGN'), u"₦ -1.20")
127        self.assertEqual(format_amount(D("1234.5"), 'YEN'), u"YEN 1,234.50")
128
129
130class FunctionalHelperTests(FunctionalTestCase):
131
132    layer = FunctionalLayer
133
134    def test_services_lister_is_registered(self):
135        # a lister of gateway services is registered on startup
136        util = queryUtility(IPaymentGatewayServicesLister)
137        assert util is not None
138
139    def test_services_are_really_listed(self):
140        # we can really get locally registered gateways when calling
141        util = queryUtility(IPaymentGatewayServicesLister)
142        assert len(util()) > 0
143
144    def test_get_payment(self):
145        # we can lookup payments.
146        self.getRootFolder()['app'] = Company()
147        app = self.getRootFolder()['app']
148        setSite(app)
149        p1 = Payment(FakePayer(), FakePayable())
150        app['payments']['1'] = p1
151        p_id = p1.payment_id
152        result = get_payment(p_id)
153        self.assertTrue(result is p1)
154        self.assertTrue(get_payment('not-valid') is None)
155
156    def test_get_payments_from_payer_id(self):
157        # we can lookup payments from payer ids.
158        self.getRootFolder()['app'] = Company()
159        app = self.getRootFolder()['app']
160        setSite(app)
161        p1 = Payment(FakePayer(), FakePayable())
162        app['payments']['1'] = p1
163        result = get_payments_from_payer_id('PAYER_01')
164        self.assertTrue(result[0] is p1)
165        self.assertTrue(get_payments_from_payer_id('not-valid') is None)
166
167    def test_get_payments_from_payable_id(self):
168        # we can lookup payments from payable ids.
169        self.getRootFolder()['app'] = Company()
170        app = self.getRootFolder()['app']
171        setSite(app)
172        p1 = Payment(FakePayer(), FakePayable())
173        app['payments']['1'] = p1
174        result = get_payments_from_payable_id('PAYABLE_01')
175        self.assertTrue(result[0] is p1)
176        self.assertTrue(get_payments_from_payable_id('not-valid') is None)
177
178    def test_find_payable_from_payable_id(self):
179        # we can find payables.
180        obj1 = object()
181        obj2 = object()
182
183        class FakeFinder(object):
184            valid = {'id1': obj1, 'id3': obj2}
185
186            def get_payable_by_id(self, the_id):
187                return self.valid.get(the_id)
188
189        finder1 = FakeFinder()
190        finder1.valid = {'id1': obj1}
191        finder2 = FakeFinder()
192        finder2.valid = {'id2': obj2}
193        gsm = getGlobalSiteManager()
194        try:
195            gsm.registerUtility(finder1, provided=IPayableFinder, name='f1')
196            gsm.registerUtility(finder2, provided=IPayableFinder, name='f2')
197            result1 = find_payable_from_payable_id('id1')
198            result2 = find_payable_from_payable_id('id2')
199            result3 = find_payable_from_payable_id('id3')
200        finally:
201            gsm.unregisterUtility(finder1, IPayableFinder)
202            gsm.unregisterUtility(finder2, IPayableFinder)
203        self.assertTrue(result1 is obj1)
204        self.assertTrue(result2 is obj2)
205        self.assertTrue(result3 is None)
206
207    def test_find_payer_from_payer_id(self):
208        # we can find payables.
209        obj1 = object()
210        obj2 = object()
211
212        class FakeFinder(object):
213            valid = {'id1': obj1, 'id3': obj2}
214
215            def get_payer_by_id(self, the_id):
216                return self.valid.get(the_id)
217
218        finder1 = FakeFinder()
219        finder1.valid = {'id1': obj1}
220        finder2 = FakeFinder()
221        finder2.valid = {'id2': obj2}
222        gsm = getGlobalSiteManager()
223        try:
224            gsm.registerUtility(finder1, provided=IPayerFinder, name='f1')
225            gsm.registerUtility(finder2, provided=IPayerFinder, name='f2')
226            result1 = find_payer_from_payer_id('id1')
227            result2 = find_payer_from_payer_id('id2')
228            result3 = find_payer_from_payer_id('id3')
229        finally:
230            gsm.unregisterUtility(finder1, IPayerFinder)
231            gsm.unregisterUtility(finder2, IPayerFinder)
232        self.assertTrue(result1 is obj1)
233        self.assertTrue(result2 is obj2)
234        self.assertTrue(result3 is None)
235
236
237class PaymentTests(FunctionalTestCase):
238
239    layer = FunctionalLayer
240
241    def setUp(self):
242        super(PaymentTests, self).setUp()
243        self.payer = FakePayer()
244        self.payable = FakePayable()
245
246    def test_iface(self):
247        # Payments fullfill any interface contracts
248        obj = Payment(self.payer, self.payable)
249        verifyClass(IPayment, Payment)
250        verifyObject(IPayment, obj)
251
252    def test_initial_values(self):
253        # important attributes are set initially
254        payer = self.payer
255        payer.payer_id = u'PAYER_ID'
256        payable = self.payable
257        payable.payable_id = u'PAYABLE_ID'
258        payable.title = u'PAYABLE-TITLE'
259        payable.currency = 'NGN'
260        payment = Payment(payer, payable)
261        assert payment.payer_id == u'PAYER_ID'
262        assert payment.payable_id == u'PAYABLE_ID'
263        assert payment.title == u'PAYABLE-TITLE'
264        assert payment.currency == 'NGN'
265        assert isinstance(payment.creation_date, datetime.datetime)
266        assert payment.payment_date is None
267
268    def test_payment_id_unique(self):
269        # we get unique payment ids
270        p1 = Payment(self.payer, self.payable)
271        p2 = Payment(self.payer, self.payable)
272        id1, id2 = p1.payment_id, p2.payment_id
273        assert id1 != id2
274
275    def test_payment_id_format(self):
276        # payment ids have a special format: "PAY_<32 hex digits>"
277        id1 = Payment(self.payer, self.payable).payment_id
278        assert isinstance(id1, basestring)
279        assert re.match('PAY_[0-9a-f]{32}', id1)
280
281    def test_initial_state_is_unpaid(self):
282        # the initial state of payments is <unpaid>
283        p1 = Payment(self.payer, self.payable)
284        assert p1.state == STATE_UNPAID
285
286    def test_approve(self):
287        # we can approve payments
288        p1 = Payment(self.payer, self.payable)
289        p1.approve()
290        assert p1.state == STATE_PAID
291        assert p1.payment_date is not None
292        assert isinstance(p1.payment_date, datetime.datetime)
293
294    def test_approve_datetime_given(self):
295        # we can give a datetime
296        p1 = Payment(self.payer, self.payable)
297        some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
298        p1.approve(payment_date=some_datetime)
299        assert p1.payment_date == some_datetime
300
301    def test_approve_datetime_automatic(self):
302        # if we do not give a datetime, current one will be used
303        current = datetime.datetime.utcnow()
304        p1 = Payment(self.payer, self.payable)
305        p1.approve()
306        assert p1.payment_date >= current
307
308    def test_mark_failed(self):
309        # we can mark payments as failed
310        p1 = Payment(self.payer, self.payable)
311        p1.mark_failed()
312        assert p1.state == STATE_FAILED
313
314    def test_amount(self):
315        # the amount of a payment is the sum of amounts of its items
316        payable = self.payable
317        payable.payment_items[0].amount = decimal.Decimal("12.25")
318        payable.payment_items[1].amount = decimal.Decimal("0.5")
319        p1 = Payment(self.payer, self.payable)
320        assert p1.amount == decimal.Decimal("12.75")
321
322    def test_amount_negative(self):
323        # we can sum up negative numbers
324        payable = self.payable
325        payable.payment_items[0].amount = decimal.Decimal("2.21")
326        payable.payment_items[1].amount = decimal.Decimal("-3.23")
327        p1 = Payment(self.payer, payable)
328        assert p1.amount == decimal.Decimal("-1.02")
329
330    def test_amount_empty(self):
331        # the amount of zero items is None.
332        payable = FakePayable(payment_items=())
333        p1 = Payment(self.payer, payable)
334        self.assertEqual(p1.amount, decimal.Decimal("0.00"))
335
336    def test_amount_changed_after_init(self):
337        # amount is dynamic: changes to payment items are reflected
338        payable = FakePayable(payment_items=())
339        p1 = Payment(self.payer, payable)
340        assert p1.amount == decimal.Decimal("0.00")
341        p1.payment_items = (PaymentItem(amount=decimal.Decimal("5.55")), )
342        assert p1.amount == decimal.Decimal("5.55")
343        p1.payment_items = (
344            PaymentItem(amount=decimal.Decimal("5.55")),
345            PaymentItem(amount=decimal.Decimal("1.11")),
346            )
347        assert p1.amount == decimal.Decimal("6.66")
348
349    def test_payment_items(self):
350        # we can get payment items from a payment
351        payable = FakePayable(payment_items=FAKE_PAYMENT_ITEMS)
352        p1 = Payment(self.payer, payable)
353        assert isinstance(p1.payment_items, tuple)
354
355    def test_payment_items_number(self):
356        # payment item values are respected
357        payable = FakePayable()
358        item = PaymentItem(amount=decimal.Decimal("9.99"))
359        payable.payment_items = [item, ]
360        payment = Payment(self.payer, payable)
361        assert len(payment.payment_items) == 1
362        assert payment.payment_items[0] is item
363
364
365class PaymentItemTests(unittest.TestCase):
366
367    def test_iface(self):
368        # PaymentItems fullfill any interface contracts
369        obj = PaymentItem()
370        verifyClass(IPaymentItem, PaymentItem)
371        verifyObject(IPaymentItem, obj)
372
373    def test_to_string(self):
374        # we can turn default PaymentItems into strings
375        obj = PaymentItem()
376        self.assertEqual(
377            obj.to_string(), u"(u'0', u'', u'0.00')")
378
379    def test_to_string_none_values(self):
380        # 'None' values are properly represented by to_string()
381        obj = PaymentItem()
382        obj.item_id = None
383        self.assertEqual(
384            obj.to_string(), u"(None, u'', u'0.00')")
385
386    def test_to_string_enocded_values(self):
387        # to_string() copes with encoded strings
388        obj = PaymentItem()
389        obj.title = u'ümläut'
390        self.assertEqual(
391            obj.to_string(), u"(u'0', u'ümläut', u'0.00')")
392
393    def test_repr(self):
394        # we can get a proper representation of PaymentItem
395        obj = PaymentItem()
396        self.assertEqual(
397            repr(obj),
398            "PaymentItem(item_id=u'0', title=u'', amount=Decimal('0.00'))")
399
400    def test_repr_can_be_evaled(self):
401        # we can eval() representations
402        obj = PaymentItem(
403            item_id=u'12', title=u'My Title', amount=decimal.Decimal("1.99"))
404        representation = repr(obj)
405        self.assertEqual(
406            representation,
407            ("PaymentItem(item_id=u'12', title=u'My Title', "
408             "amount=Decimal('1.99'))")
409            )
410        from decimal import Decimal
411        new_obj = eval(representation)
412        assert new_obj is not obj
413        assert new_obj.item_id == obj.item_id == u"12"
414        assert new_obj.title == obj.title == u"My Title"
415        assert new_obj.amount == obj.amount == Decimal("1.99")
Note: See TracBrowser for help on using the repository browser.