Ignore:
Timestamp:
12 Mar 2015, 05:29:43 (10 years ago)
Author:
uli
Message:

Merge changes from uli-payments back into trunk.

Location:
main/waeup.ikoba/trunk/src/waeup/ikoba/payments
Files:
7 edited
1 copied

Legend:

Unmodified
Added
Removed
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py

    r12671 r12741  
     1import datetime
    12import grok
     3import random
     4import re
     5from zope.event import notify
    26from waeup.ikoba.interfaces import MessageFactory as _
    3 from waeup.ikoba.payments.interfaces import IPaymentGatewayService
    4 from waeup.ikoba.payments.payment import Payment
     7from waeup.ikoba.browser.layout import IkobaEditFormPage, action
     8from waeup.ikoba.payments.interfaces import (
     9    IPaymentGatewayService, IPayment, IPayable, IPayer, IPayee,
     10    STATE_AWAITING_GATEWAY_CONFIRM, STATE_PAID)
     11from waeup.ikoba.payments.payment import (
     12    Payment, get_payment, find_payable_from_payable_id, format_amount,
     13    find_payer_from_payer_id, PaymentProviderServiceBase,
     14    PaymentWaitingForGatewayEvent, PaymentFinishedEvent)
    515
    616
    7 class DemoCreditcardPaymentService(grok.GlobalUtility):
     17grok.templatedir('templates')
     18
     19
     20WARN_FINAL_SUBMIT = _(
     21    'You can not edit your contract after final submission. '
     22    'Do you really want to submit?'
     23)
     24
     25
     26RE_CC_NUMBER = re.compile('^[0-9]{9,25}$')
     27RE_CSC = re.compile('^[0-9]{3,4}$')
     28
     29
     30class DemoCreditcardPaymentService(PaymentProviderServiceBase):
    831    """A demo payment gateway service.
    932
     
    1336    grok.name('demo_creditcard')
    1437
    15     title = _(u'Demo Creditcard')
     38    title = _(u'Credit Card (Demo Payments)')
    1639
    17     def create_payment(self, payer, payment_item_list=[],  payee=None):
     40    def create_payment(self, payer, payable,  payee=None):
    1841        """Create a payment.
    1942        """
    20         payment = Payment()
     43        if not IPayer.providedBy(payer):
     44            payer = IPayer(payer)
     45        if not IPayable.providedBy(payable):
     46            payable = IPayable(payable)
     47        if (payee is not None) and (not IPayee.providedBy(payee)):
     48            payee = IPayee(payee)
     49        payment = Payment(payer, payable, payee)
    2150        payment.gateway_service = 'demo_creditcard'  # must be grok.name above
    22         payment.payer_id = payer.payer_id
    23         for item in payment_item_list:
    24             payment.add_payment_item(item)
    2551        return payment
     52
     53    def next_step(self, payment_id):
     54        """Tell where to go next.
     55
     56        Returns (context, view_name). Both may be none.
     57        """
     58        payment = get_payment(payment_id)
     59        if payment is None:
     60            return None, None
     61        if payment.state == STATE_AWAITING_GATEWAY_CONFIRM:
     62            return payment, 'demo_cc2'
     63        return payment, 'demo_cc1'
     64
     65
     66class CreditCardStep1(IkobaEditFormPage):
     67    grok.context(IPayment)
     68    grok.name('demo_cc1')
     69    # XXX: Use own permissions for payments
     70    grok.require('waeup.Authenticated')
     71    label = "Enter Credit Card Details"
     72    grok.template('demo_cc_step1')
     73    pnav = 4
     74
     75    def validate_form(self):
     76        fields = ['first_name', 'last_name', 'cc_number', 'csc', 'exp_date']
     77        cls = 'form-group'
     78        if len(self.request.form):
     79            cls += ' has-success'
     80        result = dict([(x, cls) for x in fields])
     81        if not len(self.request.form):
     82            return True, result
     83        err = 'form-group has-error'
     84        if not self.first_name:
     85            result['first_name'] = err
     86        if not self.last_name:
     87            result['last_name'] = err
     88        if not RE_CC_NUMBER.match(self.cc_number):
     89            result['cc_number'] = err
     90        if not RE_CSC.match(self.csc):
     91            result['csc'] = err
     92        if err in result.values():
     93            return False, result
     94        return True, result
     95
     96    def update(self, first_name=None, last_name=None, cc_number=None,
     97               month=None, year=None):
     98        self.payer = IPayer(find_payer_from_payer_id(self.context.payer_id))
     99        self.payable = IPayable(find_payable_from_payable_id(
     100            self.context.payable_id))
     101        form = self.request.form
     102        self.first_name = form.get('first_name', self.payer.first_name)
     103        self.last_name = form.get('last_name', self.payer.last_name)
     104        self.cc_number = form.get('cc_number', '')
     105        self.month = int(form.get('exp_month', datetime.datetime.now().month))
     106        self.year = int(form.get('exp_year', datetime.datetime.now().year))
     107        self.csc = form.get('csc', '')
     108        self.amount = format_amount(
     109            self.context.amount, self.context.currency)
     110        self.months = ''.join([
     111            '<option%s>%s</option>' % (
     112                (x == self.month) and ' selected="selected"' or '',
     113                x) for x in range(1, 13)])
     114        self.years = ''.join([
     115            '<option%s>%s</option>' % (
     116                (x == self.year + 1) and ' selected="selected"' or '',
     117                x) for x in range(self.year - 1, self.year + 15)])
     118        self.ok, self.validations = self.validate_form()
     119
     120    @action(_('Authorize Payment'), warning=WARN_FINAL_SUBMIT,
     121            style="primary")
     122    def authorize(self, **data):
     123        if not self.ok:
     124            self.flash(_("Please review (red) entries below!"),
     125                       type='warning')
     126            return
     127        # XXX: payment really started, do lots of logging
     128        self.context.state = STATE_AWAITING_GATEWAY_CONFIRM
     129        notify(PaymentWaitingForGatewayEvent(self.context))
     130        self.redirect(self.url(self.context, 'demo_cc2'))
     131        return
     132
     133    @action(_('Cancel'))
     134    def cancel(self, **data):
     135        """Go back to the payable (if possible) or site home.
     136        """
     137        payable_id = getattr(self.context, 'payable_id', '')
     138        payed_item = find_payable_from_payable_id(payable_id)
     139        self.flash(_("Payment cancelled."))
     140        if payed_item is not None:
     141            # remove context/payment?
     142            target = payed_item
     143        else:
     144            target = grok.getSite()
     145        self.redirect(self.url(target))
     146
     147
     148class CreditCardStep2(IkobaEditFormPage):
     149    grok.context(IPayment)
     150    grok.name('demo_cc2')
     151    # XXX: Use own permissions for payments
     152    grok.require('waeup.Authenticated')
     153    label = "&nbsp;"
     154    grok.template('demo_cc_step2')
     155    pnav = 4
     156
     157    def update(self):
     158        cnt = int(self.request.form.get('cnt', '0'))
     159        self.cnt = cnt + 1
     160        threshold = random.choice(range(10))
     161        self.success = False
     162        # HERE WOULD WE REALLY ASK FOR VERIFICATION
     163        if threshold <= cnt:
     164            self.success = True
     165            self.flash(_("Your payment was finished successfully."))
     166        if self.request.form.get('SUBMIT', None):
     167            self.renew()
     168
     169    def renew(self):
     170        if not self.success:
     171            return
     172        payable_id = getattr(self.context, 'payable_id', '')
     173        payed_item = find_payable_from_payable_id(payable_id)
     174        self.context.state = STATE_PAID
     175        notify(PaymentFinishedEvent(self.context))
     176        self.redirect(self.url(payed_item))
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/interfaces.py

    r12671 r12741  
    2020from zope import schema
    2121from zope.component import getUtilitiesFor
    22 from zope.container.interfaces import IContainer
    23 from zope.container.constraints import contains
     22from zope.component.interfaces import IObjectEvent
    2423from zope.interface import Interface, Attribute
    2524from waeup.ikoba.interfaces import (
     
    3029#: Possible states of payments
    3130STATE_UNPAID = 1
    32 STATE_PAID = 2
    33 STATE_FAILED = 4
     31STATE_AWAITING_USER_CONFIRM = 2
     32STATE_AWAITING_GATEWAY_CONFIRM = 4
     33STATE_PAID = 64
     34STATE_FAILED = 128
    3435
    3536payment_states = SimpleIkobaVocabulary(
    3637    (_('Not yet paid'), STATE_UNPAID),
     38    (_('Waiting for user confirm'), STATE_AWAITING_USER_CONFIRM),
     39    (_('Waiting for verification'), STATE_AWAITING_GATEWAY_CONFIRM),
    3740    (_('Paid'), STATE_PAID),
    3841    (_('Failed'), STATE_FAILED),
     
    4043
    4144
     45class IPaymentWaitingForGatewayEvent(IObjectEvent):
     46    """Fired when a payment starts waiting for verification.
     47    """
     48    object = Attribute("""The payment waiting.""")
     49
     50
     51class IPaymentFinishedEvent(IObjectEvent):
     52    """Fired when a payment failed or succeeded.
     53    """
     54    object = Attribute("""The payment finished.""")
     55
     56
     57class IPaymentAborted(IObjectEvent):
     58    """Fired when a payment was aborted before external transactions.
     59    """
     60    object = Attribute("""The payment aborted.""")
     61
     62
    4263class PaymentGatewayServicesSource(BasicSourceFactory):
    4364    """A source that lists available payment services.
     
    88109        )
    89110
    90     def create_payment(payer, payment_item_list, payee):
     111    def create_payment(payer, payable, payee):
    91112        """Create a payment.
    92113
    93114        For all parameters we expect an object, that implements
    94         `IPayer`, `IPaymentItem`, or `IPayee` respectively. If not,
     115        `IPayer`, `IPayable`, or `IPayee` respectively. If not,
    95116        then the given objects must be at least adaptable to the
    96117        respective interface.
     
    102123        """
    103124
     125    def next_step(payment_id):
     126        """Returns a payment (as context) and a view name.
     127
     128        May result in (None, None).
     129        """
     130
     131    def store(payment):
     132        """Store `payment` in site.
     133        """
     134
    104135
    105136class IPaymentGatewayServicesLister(Interface):
     
    136167        required=True,
    137168        )
     169
     170
     171class IPayableFinder(Interface):
     172    """Finds payables.
     173
     174    For each type of content you want to make payable, you should
     175    define an IPayableFinder that can lookup payables in the
     176    site.
     177
     178    This enables access from payments (which store payable ids only)
     179    to arbitrary content objects (for which a payable finder is
     180    registered under some name).
     181
     182    The other way round (getting an id and other relevant data from
     183    any content object) is ensured by IPayable adapters.
     184    """
     185    def get_payable_by_id(item_id):
     186        """Get an item by its Id, or none.
     187        """
     188
     189
     190class IPayerFinder(Interface):
     191    """Finds payers.
     192
     193    For each type of content you understand as payer, you should
     194    define an IPayrtFinder that can lookup payers in the site.
     195
     196    This enables access from payments (which store payer ids only)
     197    to arbitrary content objects (for which a payer finder is
     198    registered under some name.
     199
     200    The other way round (getting an id and other relevant data from
     201    any content object) is ensured by IPayer adapters.
     202    """
     203    def get_payer_by_id(item_id):
     204        """Get a payer by its Id, or none.
     205        """
    138206
    139207
     
    161229
    162230
    163 class IPayment(IContainer):
     231class IPayable(Interface):
     232    """Something that can be payed.
     233
     234    Designed to serve as adapter. IPayables turn arbitrary content
     235    objects into something with a standarized interfaces for use with
     236    payments.
     237
     238    While currency is important to tell about the amount currency, the
     239    total amount is computed on-demand from payment items.
     240    """
     241    payable_id = schema.TextLine(
     242        title=u'ID of a payable',
     243        description=(u'It should be possible to lookup the payable id '
     244                     u'by some registered IPayableFinder later on'),
     245        required=True,
     246        readonly=True,
     247        )
     248
     249    title = schema.TextLine(
     250        title=u'Title',
     251        description=u'A short description of the payed item.',
     252        required=True,
     253        default=u'',
     254        readonly=True,
     255        )
     256
     257    currency = schema.Choice(
     258        title=u'Currency',
     259        source=ISO_4217_CURRENCIES_VOCAB,
     260        required=True,
     261        default='USD',
     262        readonly=True,
     263        )
     264
     265    payment_items = schema.Tuple(
     266        title=u'Tuple of IPaymentItems.',
     267        value_type=schema.Object(
     268            title=u'Payment Item',
     269            schema=IPaymentItem,
     270            ),
     271        required=False,
     272        default=(),
     273        readonly=True,
     274        )
     275
     276
     277class IPayment(IIkobaObject):
    164278    """A base representation of payments.
    165279
     
    185299    we mark the payment 'failed'.
    186300    """
    187     contains(IPaymentItem)
    188 
    189301    payment_id = schema.TextLine(
    190302        title=u'Payment Identifier',
    191303        default=None,
     304        required=True,
     305        )
     306
     307    title = schema.TextLine(
     308        title=u'Payment description.',
     309        default=u'',
    192310        required=True,
    193311        )
     
    203321        default=None,
    204322        required=False,
    205     )
     323        )
     324
     325    payable_id = schema.TextLine(
     326        title=u'ID of item/good being paid',
     327        default=None,
     328        required=False,
     329        )
    206330
    207331    gateway_service = schema.Choice(
     
    256380        """
    257381
    258     def add_payment_item(item):
    259         """Payments contain payment items.
    260 
    261         Add one
    262         """
    263 
    264382
    265383class IPayer(Interface):
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py

    r12671 r12741  
    2323import uuid
    2424from datetime import datetime
    25 from zope.component import getUtilitiesFor
     25from zope.catalog.interfaces import ICatalog
     26from zope.component import getUtilitiesFor, getUtility
    2627from zope.event import notify
     28from waeup.ikoba.interfaces import MessageFactory as _
    2729from waeup.ikoba.utils.helpers import attrs_to_fields
     30from waeup.ikoba.utils.logger import Logger
    2831from waeup.ikoba.payments.interfaces import (
    2932    IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
    3033    IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
    31     IPaymentGatewayServicesLister,
     34    IPaymentGatewayServicesLister, IPayableFinder, IPayerFinder,
     35    IPaymentWaitingForGatewayEvent, IPaymentFinishedEvent,
    3236    )
    33 from waeup.ikoba.utils.logger import Logger
     37
     38
     39def format_amount(amount, currency):
     40    """Turn `amount`, `currency` into a readable string.
     41    """
     42    cncy_map = {'USD': u'US$', 'EUR': u'\u20ac', 'NGN': u'\u20a6'}
     43    currency = cncy_map.get(currency, currency)
     44    return '%s %s' % (currency, '{:,.2f}'.format(amount))
     45
     46
     47def get_payment(payment_id):
     48    """Get payment by payment id.
     49
     50    If no such payment can be found in catalog, return none.
     51    """
     52    cat = getUtility(ICatalog, name='payments_catalog')
     53    result_set = [x for x in cat.searchResults(
     54        payment_id=(payment_id, payment_id))]
     55    if len(result_set):
     56        return result_set[0]
     57    return None
     58
     59
     60def find_payable_from_payable_id(payable_id):
     61    """Find a payable from its id.
     62
     63    Looks up all registered IPayableFinders and returns the first
     64    positive result found.
     65    """
     66    for name, util in getUtilitiesFor(IPayableFinder):
     67        result = util.get_payable_by_id(payable_id)
     68        if result is not None:
     69            return result
     70    return None
     71
     72
     73def find_payer_from_payer_id(payer_id):
     74    """Find a payer from its id.
     75
     76    Looks up all registered IPayerFinders and returns the first
     77    positive result found.
     78    """
     79    for name, util in getUtilitiesFor(IPayerFinder):
     80        result = util.get_payer_by_id(payer_id)
     81        if result is not None:
     82            return result
     83    return None
     84
     85
     86def format_payment_item_values(payment_item_values, currency):
     87    """Format tuples (description, currency, amount) for output.
     88
     89    `currency` passed in is the 'target' currency.
     90
     91    Returns a list of formated values. Last item is total sum.
     92    XXX: we do not really respect currency. If different items
     93         have different currencies, we are choked.
     94    """
     95    result = []
     96    total = decimal.Decimal("0.00")
     97    for descr, item_currency, amount in payment_item_values:
     98        total += amount
     99        if item_currency != currency:
     100            raise ValueError(
     101                "Different currencies in payment items not supported.")
     102        result.append((descr, '%s %0.2f' % (item_currency, amount)))
     103    result.append((_('Total'), '%s %0.2f' % (currency, total)))
     104    return result
    34105
    35106
     
    42113
    43114
     115class PaymentWaitingForGatewayEvent(object):
     116    grok.implements(IPaymentWaitingForGatewayEvent)
     117
     118    def __init__(self, obj):
     119        self.object = obj
     120
     121
     122class PaymentFinishedEvent(object):
     123    grok.implements(IPaymentFinishedEvent)
     124
     125    def __init__(self, obj):
     126        self.object = obj
     127
     128
    44129class PaymentGatewayServicesLister(grok.GlobalUtility):
    45130    grok.implements(IPaymentGatewayServicesLister)
     
    52137
    53138class PaymentProviderServiceBase(grok.GlobalUtility):
    54 
     139    """Base for IPaymentGatewayServices.
     140    """
    55141    grok.baseclass()
    56142    grok.implements(IPaymentGatewayService)
     
    58144    title = u'Sample Credit Card Service'
    59145
    60 
    61 @attrs_to_fields
    62 class Payment(grok.Container, Logger):
     146    def store(self, payment):
     147        """Store `payment` in site.
     148        """
     149        site = grok.getSite()
     150        payments = site['payments']
     151        if payment.payment_id in payments:
     152            del site['payments'][payment.payment_id]
     153        site['payments'][payment.payment_id] = payment
     154
     155
     156@attrs_to_fields
     157class Payment(grok.Model, Logger):
    63158    """This is a payment.
    64159    """
     
    70165    logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
    71166
    72     @property
    73     def amount(self):
    74         """The amount of a payment.
    75 
    76         Equals the sum of items contained.
    77         """
    78         return sum(
    79             [item.amount for item in self.values()],
    80             decimal.Decimal("0.00")  # default value
    81         )
    82 
    83     def __init__(self):
     167    def __init__(self, payer, payable, payee=None):
    84168        super(Payment, self).__init__()
     169        item_amounts = [decimal.Decimal("0.00"), ]
     170        item_amounts += [item.amount for item in payable.payment_items]
     171        self.amount = sum(item_amounts)
     172        self.payer_id = payer.payer_id
     173        self.payable_id = payable.payable_id
     174        self.title = payable.title
    85175        self.creation_date = datetime.utcnow()
    86176        self.payment_date = None
    87177        self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex)
    88178        self.state = STATE_UNPAID
     179        self.currency = payable.currency
     180        if payee is not None:
     181            self.payee_id = payee.payee_id
    89182        return
    90183
     
    113206        notify(grok.ObjectModifiedEvent(self))
    114207
    115     def add_payment_item(self, item):
    116         """Add `item`
    117 
    118         Returns the key under which the `item` was stored. Please do
    119         not make anby assumptions about the key. It will be a
    120         string. That is all we can tell.
    121 
    122         """
    123         cnt = 0
    124         while str(cnt) in self:
    125             cnt += 1
    126         self[str(cnt)] = item
    127         return str(cnt)
    128 
    129208
    130209@attrs_to_fields
     
    138217
    139218@attrs_to_fields
    140 class PaymentItem(grok.Model):
     219class PaymentItem(object):
    141220
    142221    grok.implements(IPaymentItem)
    143222
    144     def __init__(self):
     223    def __init__(
     224            self, item_id=u"0", title=u"", amount=decimal.Decimal("0.00")):
    145225        super(PaymentItem, self).__init__()
     226        self.item_id = item_id
     227        self.title = title
     228        self.amount = amount
    146229
    147230
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py

    r12498 r12741  
    3535    IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
    3636    )
     37from waeup.ikoba.payments.payment import PaymentProviderServiceBase
    3738from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB
    3839from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB
     
    11411142
    11421143
    1143 class PayPalCreditCardService(grok.GlobalUtility):
     1144class PayPalCreditCardService(PaymentProviderServiceBase):
    11441145    grok.implements(IPaymentGatewayService)
    11451146    grok.name('paypal_creditcard')
     
    12161217        return payment
    12171218
    1218 
    1219 class PayPalRegularPaymentService(grok.GlobalUtility):
     1219    def next_step(self, payment_id):
     1220        raise NotImplemented("next_steo() not implemented")
     1221
     1222
     1223class PayPalRegularPaymentService(PaymentProviderServiceBase):
    12201224    grok.implements(IPaymentGatewayService)
    12211225    grok.name('paypal_regular')
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_demo_provider.py

    r12671 r12741  
    1 import decimal
     1import unittest
     2from zope.component import queryUtility
     3from zope.component.hooks import setSite
    24from zope.interface.verify import verifyClass, verifyObject
    35from waeup.ikoba.testing import (
     
    79    IPaymentGatewayService, IPayment, STATE_UNPAID,
    810    )
    9 from waeup.ikoba.payments.payment import Payer, PaymentItem, Payee
     11from waeup.ikoba.app import Company
     12from waeup.ikoba.payments.payment import Payer, Payee, Payment
    1013from waeup.ikoba.payments.demo_provider import (
    11     DemoCreditcardPaymentService,
     14    DemoCreditcardPaymentService, RE_CC_NUMBER, RE_CSC,
    1215    )
     16from waeup.ikoba.payments.tests.test_payment import FakePayer, FakePayable
    1317
    1418
    15 class DemoCreditcarPaymentServiceTests(FunctionalTestCase):
     19class TestDemoProviderHelpers(unittest.TestCase):
     20
     21    def test_re_cc_number(self):
     22        # we recognize valid numbers
     23        assert RE_CC_NUMBER.match('a') is None
     24        assert RE_CC_NUMBER.match('12345678') is None
     25        assert RE_CC_NUMBER.match('1234a5678') is None
     26        assert RE_CC_NUMBER.match('132456789') is not None
     27        assert RE_CC_NUMBER.match('123456789012345') is not None
     28
     29    def test_re_csc(self):
     30        # we recognize security numbers
     31        assert RE_CSC.match('12') is None
     32        assert RE_CSC.match('123') is not None
     33        assert RE_CSC.match('1234') is not None
     34        assert RE_CSC.match('12345') is None
     35        assert RE_CSC.match('12A2') is None
     36
     37
     38class DemoCreditcardPaymentServiceTests(FunctionalTestCase):
    1639
    1740    layer = FunctionalLayer
     41
     42    def setUp(self):
     43        super(DemoCreditcardPaymentServiceTests, self).setUp()
     44        self.app = Company()
     45        self.getRootFolder()['app'] = self.app
     46        setSite(self.app)
    1847
    1948    def test_iface(self):
     
    2655        # we can get payments from payment gateways
    2756        service = DemoCreditcardPaymentService()
    28         payer, payment_item, payee = Payer(), PaymentItem(), Payee()
     57        payer, payable, payee = Payer(), FakePayable(), Payee()
    2958        payer.payer_id = u'SOME_PAYER_ID'
    30         result = service.create_payment(payer, [], payee)
     59        result = service.create_payment(payer, payable, payee)
    3160        assert IPayment.providedBy(result)
    3261        assert result.gateway_service == u'demo_creditcard'
    3362        assert result.state == STATE_UNPAID
    34         assert len(result) == 0  # no items stored
    35 
    36     def test_create_payment_honors_payment_item(self):
    37         # we inspect payment items and take their values
    38         service = DemoCreditcardPaymentService()
    39         payer, payment_item, payee = Payer(), PaymentItem(), Payee()
    40         payment_item.item_id = u'SOME_ITEM_ID'
    41         payer.payer_id = u'SOME_PAYER_ID'
    42         payment_item.amount = decimal.Decimal("300.99")
    43         result = service.create_payment(payer, [payment_item], payee)
    44         self.assertEqual(result.amount, payment_item.amount)
    45         assert len(result) == 1
    4663
    4764    def test_create_payment_honors_payer(self):
    4865        # we inspect payers when creating their payments
    4966        service = DemoCreditcardPaymentService()
    50         payer, payment_item, payee = Payer(), PaymentItem(), Payee()
    51         payment_item.item_id = u'SOME_ITEM_ID'
     67        payer, payable, payee = Payer(), FakePayable(), Payee()
    5268        payer.payer_id = u'SOME_PAYER_ID'
    53         result = service.create_payment(payer, [payment_item], payee)
     69        result = service.create_payment(payer, payable, payee)
    5470        assert result.payer_id == payer.payer_id
    5571
    5672    def test_get_service_by_utility_name(self):
    5773        # we can get the demo credit card service by its utility name
    58         from zope.component import queryUtility
    5974        service = queryUtility(
    6075            IPaymentGatewayService, name="demo_creditcard")
    6176        assert service is not None
     77
     78    def test_next_step_invalid_id(self):
     79        # we cannot go to non-existent payments
     80        service = DemoCreditcardPaymentService()
     81        result = service.next_step('not-existent-payment-id')
     82        assert result == (None, None)
     83
     84    def test_next_step(self):
     85        # we are redirected to 'index' in the beginning
     86        service = DemoCreditcardPaymentService()
     87        p1 = Payment(FakePayer(), FakePayable())
     88        self.app['payments']['1'] = p1
     89        p_id = p1.payment_id
     90        result = service.next_step(p_id)
     91        assert result == (p1, 'demo_cc1')
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_payment.py

    r12671 r12741  
     1# -*- coding: utf-8 -*-
    12## $Id$
    23##
     
    2021import re
    2122import unittest
    22 from zope.component import getUtilitiesFor, getSiteManager, queryUtility
    23 from zope.interface import implements
     23from zope.component import (
     24    getUtilitiesFor, getSiteManager, queryUtility, getGlobalSiteManager,
     25    )
     26from zope.component.hooks import setSite
     27from zope.interface import implements, implementer
    2428from zope.interface.verify import verifyClass, verifyObject
    2529from waeup.ikoba.payments.interfaces import (
    2630    IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED,
    2731    IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister,
     32    IPayableFinder, IPayerFinder, IPayable, IPayer,
    2833    )
     34from waeup.ikoba.app import Company
    2935from waeup.ikoba.payments.payment import (
    30     Payment, get_payment_providers, PaymentItem,
     36    Payment, get_payment_providers, PaymentItem, format_payment_item_values,
     37    get_payment, find_payable_from_payable_id, find_payer_from_payer_id,
     38    format_amount,
    3139    )
    3240from 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    items = (
     64        (u'item 1', decimal.Decimal("1.00")),
     65        (u'item 2', decimal.Decimal("2.12")),
     66        )
     67
     68    def __init__(self, payable_id=u'PAYABLE_01', title=u'title',
     69                 currency=u'USD', payment_items=FAKE_PAYMENT_ITEMS):
     70        self.payable_id = payable_id
     71        self.title = title
     72        self.currency = currency
     73        self.payment_items = payment_items
    3374
    3475
     
    61102        assert result['some_name'] is fake_util
    62103
     104    def test_format_payment_item_values(self):
     105        # we can format lists of payment item values
     106        result = format_payment_item_values(
     107            [(u'Item 1', 'USD', decimal.Decimal("12.123")),
     108             (u'Item 2', 'USD', decimal.Decimal("12.002")),
     109             ], 'USD')
     110        self.assertEqual(
     111            result, [(u'Item 1', 'USD 12.12'),
     112                     (u'Item 2', 'USD 12.00'),
     113                     (u'Total', 'USD 24.12')]
     114            )
     115
     116    def test_format_payment_item_values_req_single_currency(self):
     117        # we require one currency for all items, yet.
     118        self.assertRaises(
     119            ValueError, format_payment_item_values,
     120            [(u'Item 1', 'USD', decimal.Decimal("12.12")),
     121             (u'Item 2', 'EUR', decimal.Decimal("50")),
     122             ],
     123            'USD')
     124
     125    def test_format_amount(self):
     126        # we can make amounts readable
     127        D = decimal.Decimal
     128        self.assertEqual(format_amount(D("0"), 'USD'), u"US$ 0.00")
     129        self.assertEqual(format_amount(D("0.1"), 'EUR'), u"€ 0.10")
     130        self.assertEqual(format_amount(D("-1.2"), 'NGN'), u"₦ -1.20")
     131        self.assertEqual(format_amount(D("1234.5"), 'YEN'), u"YEN 1,234.50")
     132
    63133
    64134class FunctionalHelperTests(FunctionalTestCase):
     
    76146        assert len(util()) > 0
    77147
    78 
    79 class PaymentTests(unittest.TestCase):
     148    def test_get_payment(self):
     149        # we can lookup payments.
     150        self.getRootFolder()['app'] = Company()
     151        app = self.getRootFolder()['app']
     152        setSite(app)
     153        p1 = Payment(FakePayer(), FakePayable())
     154        app['payments']['1'] = p1
     155        p_id = p1.payment_id
     156        result = get_payment(p_id)
     157        self.assertTrue(result is p1)
     158        self.assertTrue(get_payment('not-valid') is None)
     159
     160    def test_find_payable_from_payable_id(self):
     161        # we can find payables.
     162        obj1 = object()
     163        obj2 = object()
     164
     165        class FakeFinder(object):
     166            valid = {'id1': obj1, 'id3': obj2}
     167
     168            def get_payable_by_id(self, the_id):
     169                return self.valid.get(the_id)
     170
     171        finder1 = FakeFinder()
     172        finder1.valid = {'id1': obj1}
     173        finder2 = FakeFinder()
     174        finder2.valid = {'id2': obj2}
     175        gsm = getGlobalSiteManager()
     176        try:
     177            gsm.registerUtility(finder1, provided=IPayableFinder, name='f1')
     178            gsm.registerUtility(finder2, provided=IPayableFinder, name='f2')
     179            result1 = find_payable_from_payable_id('id1')
     180            result2 = find_payable_from_payable_id('id2')
     181            result3 = find_payable_from_payable_id('id3')
     182        finally:
     183            gsm.unregisterUtility(finder1, IPayableFinder)
     184            gsm.unregisterUtility(finder2, IPayableFinder)
     185        self.assertTrue(result1 is obj1)
     186        self.assertTrue(result2 is obj2)
     187        self.assertTrue(result3 is None)
     188
     189    def test_find_payer_from_payer_id(self):
     190        # we can find payables.
     191        obj1 = object()
     192        obj2 = object()
     193
     194        class FakeFinder(object):
     195            valid = {'id1': obj1, 'id3': obj2}
     196
     197            def get_payer_by_id(self, the_id):
     198                return self.valid.get(the_id)
     199
     200        finder1 = FakeFinder()
     201        finder1.valid = {'id1': obj1}
     202        finder2 = FakeFinder()
     203        finder2.valid = {'id2': obj2}
     204        gsm = getGlobalSiteManager()
     205        try:
     206            gsm.registerUtility(finder1, provided=IPayerFinder, name='f1')
     207            gsm.registerUtility(finder2, provided=IPayerFinder, name='f2')
     208            result1 = find_payer_from_payer_id('id1')
     209            result2 = find_payer_from_payer_id('id2')
     210            result3 = find_payer_from_payer_id('id3')
     211        finally:
     212            gsm.unregisterUtility(finder1, IPayerFinder)
     213            gsm.unregisterUtility(finder2, IPayerFinder)
     214        self.assertTrue(result1 is obj1)
     215        self.assertTrue(result2 is obj2)
     216        self.assertTrue(result3 is None)
     217
     218
     219class PaymentTests(FunctionalTestCase):
     220
     221    layer = FunctionalLayer
     222
     223    def setUp(self):
     224        super(PaymentTests, self).setUp()
     225        self.payer = FakePayer()
     226        self.payable = FakePayable()
    80227
    81228    def test_iface(self):
    82229        # Payments fullfill any interface contracts
    83         obj = Payment()
     230        obj = Payment(self.payer, self.payable)
    84231        verifyClass(IPayment, Payment)
    85232        verifyObject(IPayment, obj)
    86233
     234    def test_initial_values(self):
     235        # important attributes are set initially
     236        payer = self.payer
     237        payer.payer_id = u'PAYER_ID'
     238        payable = self.payable
     239        payable.payable_id = u'PAYABLE_ID'
     240        payable.title = u'PAYABLE-TITLE'
     241        payable.currency = 'NGN'
     242        payment = Payment(payer, payable)
     243        assert payment.payer_id == u'PAYER_ID'
     244        assert payment.payable_id == u'PAYABLE_ID'
     245        assert payment.title == u'PAYABLE-TITLE'
     246        assert payment.currency == 'NGN'
     247        assert isinstance(payment.creation_date, datetime.datetime)
     248        assert payment.payment_date is None
     249
    87250    def test_payment_id_unique(self):
    88251        # we get unique payment ids
    89         p1, p2 = Payment(), Payment()
     252        p1 = Payment(self.payer, self.payable)
     253        p2 = Payment(self.payer, self.payable)
    90254        id1, id2 = p1.payment_id, p2.payment_id
    91255        assert id1 != id2
     
    93257    def test_payment_id_format(self):
    94258        # payment ids have a special format: "PAY_<32 hex digits>"
    95         id1 = Payment().payment_id
     259        id1 = Payment(self.payer, self.payable).payment_id
    96260        assert isinstance(id1, basestring)
    97261        assert re.match('PAY_[0-9a-f]{32}', id1)
     
    99263    def test_initial_state_is_unpaid(self):
    100264        # the initial state of payments is <unpaid>
    101         p1 = Payment()
     265        p1 = Payment(self.payer, self.payable)
    102266        assert p1.state == STATE_UNPAID
    103267
    104268    def test_approve(self):
    105269        # we can approve payments
    106         p1 = Payment()
     270        p1 = Payment(self.payer, self.payable)
    107271        p1.approve()
    108272        assert p1.state == STATE_PAID
     
    112276    def test_approve_datetime_given(self):
    113277        # we can give a datetime
    114         p1 = Payment()
     278        p1 = Payment(self.payer, self.payable)
    115279        some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0)
    116280        p1.approve(payment_date=some_datetime)
     
    120284        # if we do not give a datetime, current one will be used
    121285        current = datetime.datetime.utcnow()
    122         p1 = Payment()
     286        p1 = Payment(self.payer, self.payable)
    123287        p1.approve()
    124288        assert p1.payment_date >= current
     
    126290    def test_mark_failed(self):
    127291        # we can mark payments as failed
    128         p1 = Payment()
     292        p1 = Payment(self.payer, self.payable)
    129293        p1.mark_failed()
    130294        assert p1.state == STATE_FAILED
    131295
    132     def test_add_payment_item(self):
    133         # we can add payment items
    134         p1 = Payment()
    135         item1 = PaymentItem()
    136         result = p1.add_payment_item(item1)
    137         assert len(p1) == 1  # do not make assumptions about result content
    138         assert isinstance(result, basestring)
    139 
    140     def test_add_payment_item_multiple(self):
    141         # we can add several items
    142         p1 = Payment()
    143         item1 = PaymentItem()
    144         item2 = PaymentItem()
    145         result1 = p1.add_payment_item(item1)
    146         result2 = p1.add_payment_item(item2)
    147         assert len(p1) == 2  # do not make assumptions about result content
    148         assert isinstance(result1, basestring)
    149         assert isinstance(result2, basestring)
    150 
    151296    def test_amount(self):
    152297        # the amount of a payment is the sum of amounts of its items
    153         p1 = Payment()
    154         item1 = PaymentItem()
    155         item2 = PaymentItem()
    156         p1.add_payment_item(item1)
    157         p1.add_payment_item(item2)
    158         item1.amount = decimal.Decimal("12.25")
    159         item2.amount = decimal.Decimal("0.5")
     298        payable = self.payable
     299        payable.payment_items[0].amount = decimal.Decimal("12.25")
     300        payable.payment_items[1].amount = decimal.Decimal("0.5")
     301        p1 = Payment(self.payer, self.payable)
    160302        assert p1.amount == decimal.Decimal("12.75")
    161303
    162304    def test_amount_negative(self):
    163305        # we can sum up negative numbers
    164         p1 = Payment()
    165         item1 = PaymentItem()
    166         item2 = PaymentItem()
    167         p1.add_payment_item(item1)
    168         p1.add_payment_item(item2)
    169         item1.amount = decimal.Decimal("2.21")
    170         item2.amount = decimal.Decimal("-3.23")
     306        payable = self.payable
     307        payable.payment_items[0].amount = decimal.Decimal("2.21")
     308        payable.payment_items[1].amount = decimal.Decimal("-3.23")
     309        p1 = Payment(self.payer, payable)
    171310        assert p1.amount == decimal.Decimal("-1.02")
    172311
    173312    def test_amount_empty(self):
    174         # the amount of zero items is 0.00.
    175         p1 = Payment()
    176         assert p1.amount == decimal.Decimal("0.00")
    177         assert isinstance(p1.amount, decimal.Decimal)
     313        # the amount of zero items is None.
     314        payable = FakePayable(payment_items=())
     315        p1 = Payment(self.payer, payable)
     316        self.assertEqual(p1.amount, decimal.Decimal("0.00"))
    178317
    179318
  • main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py

    r12498 r12741  
    11741174    def test_store_credit_card_invalid(self):
    11751175        # an exception is raised with invalid credit cards.
    1176         site = self.create_site()
     1176        self.create_site()
    11771177        service = PayPalCreditCardService()
    11781178        credit_card = CreditCard(
     
    12031203        # we can actually create payments
    12041204        service = PayPalCreditCardService()
    1205         site = self.create_site()
     1205        self.create_site()
    12061206        credit_card = self.get_credit_card()
    12071207        result = service.store_credit_card(credit_card)
Note: See TracChangeset for help on using the changeset viewer.