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

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

Remove item_id from PaymentItem?.

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