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
Files:
17 edited
1 copied

Legend:

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

    r12258 r12741  
    3535from waeup.ikoba.products.container import ProductsContainer
    3636from waeup.ikoba.documents.container import DocumentsContainer
     37from waeup.ikoba.payments.container import PaymentsContainer
    3738
    3839
     
    5960        the like.
    6061        """
    61 
    6262        self['users'] = UsersContainer()
    6363        self['datacenter'] = DataCenter()
     
    6767        self['products'] = ProductsContainer()
    6868        self['documents'] = DocumentsContainer()
     69        self['payments'] = PaymentsContainer()
    6970        self._createPlugins()
    7071
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py

    r12663 r12741  
    1818"""UI components for customers and related components.
    1919"""
    20 
    21 import sys
    2220import grok
    23 import pytz
    2421import os
    2522from urllib import urlencode
    26 from datetime import datetime
    2723from zope.event import notify
    2824from zope.i18n import translate
    2925from zope.catalog.interfaces import ICatalog
    3026from zope.component import queryUtility, getUtility, createObject
    31 from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
    32 from zope.formlib.textwidgets import BytesDisplayWidget
    3327from zope.security import checkPermission
    3428from hurry.workflow.interfaces import (
     
    4236from waeup.ikoba.browser.layout import (
    4337    IkobaPage, IkobaEditFormPage, IkobaAddFormPage, IkobaDisplayFormPage,
    44     IkobaForm, NullValidator, jsaction, action, UtilityView)
    45 from waeup.ikoba.widgets.datewidget import (
    46     FriendlyDateWidget, FriendlyDateDisplayWidget,
    47     FriendlyDatetimeDisplayWidget)
     38    NullValidator, jsaction, action, UtilityView)
    4839from waeup.ikoba.browser.pages import ContactAdminForm
    4940from waeup.ikoba.browser.breadcrumbs import Breadcrumb
    5041from waeup.ikoba.browser.interfaces import ICaptchaManager
    5142from waeup.ikoba.mandates.mandate import PasswordMandate
     43from waeup.ikoba.payments.payment import format_payment_item_values
     44from waeup.ikoba.payments.interfaces import (
     45    IPaymentGatewayServicesLister, IPaymentGatewayService, IPayer, IPayable
     46    )
    5247from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget
    5348from waeup.ikoba.utils.helpers import (
    54     get_current_principal, to_timezone, now, format_date)
     49    get_current_principal, format_date)
    5550from waeup.ikoba.customers.interfaces import (
    5651    ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils,
    5752    ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate,
    5853    ICustomerPDFDocument, IContractsContainer, IContract,
    59     IContractSelectProduct, ISampleContract,
     54    IContractSelectProduct,
    6055    )
    6156from waeup.ikoba.customers.catalog import search
    6257from waeup.ikoba.customers.workflow import PAYMENT_TRANSITIONS
     58
    6359
    6460grok.context(IIkobaObject)
     
    927923    def createDocument(self, **data):
    928924        form = self.request.form
    929         customer = self.context.__parent__
    930925        doctype = form.get('doctype', None)
    931926        # Here we can create various instances of CustomerDocument derived
     
    10981093        tableheader = []
    10991094        tabledata = []
    1100         contenttitle = []
    11011095        for i in range(1,3):
    11021096            tabledata.append(sorted(
     
    11341128    @property
    11351129    def label(self):
    1136         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    11371130        return '%s of %s\nTitle: %s' % (
    11381131            self.context.translated_class_name,
     
    11411134
    11421135    def render(self):
    1143         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    11441136        customerview = CustomerBasePDFFormPage(self.context.customer,
    11451137            self.request, self.omit_fields)
     
    11571149
    11581150    def render(self):
    1159         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    11601151        customerview = CustomerBasePDFFormPage(self.context.customer,
    11611152            self.request, self.omit_fields)
     
    12951286    def createContract(self, **data):
    12961287        form = self.request.form
    1297         customer = self.context.__parent__
    12981288        contype = form.get('contype', None)
    12991289        # Here we can create various instances of Contract derived
     
    15061496    label = _('Select payment method')
    15071497
    1508     def update(self, CANCEL=None):
     1498    @property
     1499    def payment_gateways(self):
     1500        """Get an iter over registered and enabled gateway service providers.
     1501
     1502        We provide tuples ``(value, description)`` for each supported
     1503        payment gateway.
     1504        """
     1505        lister = getUtility(IPaymentGatewayServicesLister)
     1506        for name, service in lister().items():
     1507            yield {'name': name, 'title': service.title}
     1508
     1509    def update(self, CANCEL=None, gw=None):
    15091510        if self.context.state != CREATED or not self.context.fee_based:
    15101511            emit_lock_message(self)
    15111512            return
     1513        self.gw = gw
    15121514        super(SelectPaymentMethodPage, self).update()
    15131515        return
    15141516
    1515     @action(_('Select payment method and proceed to payment gateway (final submission)'),
    1516             style='primary', warning=WARNING_CON,)
     1517    @action(_('Select payment method (final submission)'),
     1518            style='primary')
    15171519    def confirm(self, **data):
    1518         IWorkflowInfo(self.context).fireTransition('await')
    1519         self.flash(_('Payment has been initiated.'))
    1520         return
     1520        if self.gw is None:
     1521            self.flash(_('Please pick a payment method.'),
     1522                       type='warning')
     1523        else:
     1524            service = queryUtility(IPaymentGatewayService, name=self.gw)
     1525            if service is None:
     1526                self.flash(_('Invalid payment gateway.'), type='danger')
     1527                return
     1528            payer = IPayer(self.context)
     1529            payable = IPayable(self.context)
     1530            payment = service.create_payment(payer, payable)
     1531            service.store(payment)
     1532            payment, view_name = service.next_step(payment.payment_id)
     1533            url = self.url(payment, view_name)
     1534            self.redirect(url)
     1535            return
     1536        return
     1537
     1538    @action(_('Cancel'))
     1539    def cancel(self, **data):
     1540        self.redirect(self.url(self.context, 'edit'))
     1541        return
     1542
    15211543
    15221544class ContractTriggerTransitionFormPage(IkobaEditFormPage):
     
    16001622        tableheader = []
    16011623        tabledata = []
    1602         contenttitle = []
    16031624        for i in range(1,3):
    16041625            tabledata.append(sorted(
     
    16551676    @property
    16561677    def label(self):
    1657         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    16581678        return self.context.title
    16591679
     
    16701690
    16711691    def render(self):
    1672         portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
    16731692        customerview = CustomerBasePDFFormPage(self.context.customer,
    16741693            self.request, self.omit_fields)
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt

    r12663 r12741  
     1
    12<form action="." tal:attributes="action request/URL" method="post"
    23      i18n:domain="waeup.ikoba" enctype="multipart/form-data">
    34
    4   <table class="form-table">
    5     <tbody>
    6       <tr>
    7         <td class="fieldname" i18n:translate=""></td>
    8         <td>
    9         </td>
    10       </tr>
    11       <tr>
    12         <td class="fieldname" i18n:translate=""></td>
    13         <td>
     5  <div class="form-group">
    146
    15         </td>
    16       </tr>
    17     </tbody>
    18   </table>
     7    <div class="radio" tal:repeat="service view/payment_gateways">
     8      <label>
     9        <input type="radio" name="gw" tal:attributes="value service/name" />
     10        <b><span tal:replace="service/title" /></b>
     11      </label>
     12    </div>
     13
     14  </div>
     15
    1916  <br />
    2017
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/contracts.py

    r12663 r12741  
    1919Customer contract components.
    2020"""
    21 import os
    2221import grok
    23 from zope.component import queryUtility, getUtility
     22from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
     23from zope.catalog.interfaces import ICatalog
     24from zope.component import getUtility, queryUtility
    2425from zope.component.interfaces import IFactory
    2526from zope.interface import implementedBy
    2627from zope.schema import getFields
    27 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
    2828from waeup.ikoba.interfaces import MessageFactory as _
    2929from waeup.ikoba.interfaces import (
    30     IIkobaUtils, IObjectHistory,
    31     VERIFIED, APPROVED, PROVISIONALLY,
    32     IIDSource)
     30    IObjectHistory, VERIFIED, APPROVED, PROVISIONALLY, IIDSource)
    3331from waeup.ikoba.customers.interfaces import (
    34     IContractsContainer, ICustomerNavigation,
    35     IContract, IContractSelectProduct, ICustomersUtils,
    36     ISampleContract, ISampleContractProcess, ISampleContractEdit,
    37     ISampleContractOfficialUse)
    38 from waeup.ikoba.customers.utils import generate_contract_id
     32    IContractsContainer, ICustomerNavigation, IContract,
     33    IContractSelectProduct, ICustomersUtils, ISampleContract,
     34    ISampleContractProcess, ISampleContractEdit, ISampleContractOfficialUse)
     35from waeup.ikoba.payments.interfaces import (
     36    IPayer, IPayableFinder, IPayable, IPaymentWaitingForGatewayEvent,
     37    STATE_PAID, STATE_FAILED, IPaymentFinishedEvent
     38    )
     39from waeup.ikoba.payments.payment import (
     40    PaymentItem, find_payable_from_payable_id,
     41    )
    3942from waeup.ikoba.utils.helpers import attrs_to_fields
     43
    4044
    4145class ContractsContainer(grok.Container):
     
    6165ContractsContainer = attrs_to_fields(ContractsContainer)
    6266
     67
     68class ContractPayer(grok.Adapter):
     69    """Adapter to turn contracts into IPayers.
     70    """
     71    grok.implements(IPayer)
     72    grok.context(IContract)
     73
     74    def __init__(self, context):
     75        self.context = context
     76
     77    @property
     78    def _customer(self):
     79        return self.context.customer
     80
     81    @property
     82    def first_name(self):
     83        return getattr(self._customer, 'firstname', None)
     84
     85    @property
     86    def last_name(self):
     87        return getattr(self._customer, 'lastname', None)
     88
     89    @property
     90    def payer_id(self):
     91        return getattr(self._customer, 'customer_id', None)
     92
     93
    6394class ContractBase(grok.Container):
    6495    """This is a customer contract baseclass.
    6596    """
    66     grok.implements(IContractSelectProduct)  # Necesary for the selectproduct page
    67 
     97    grok.implements(IContractSelectProduct)  # Neccessary for the
     98                                             # selectproduct page (why?)
    6899    grok.baseclass()
    69100
     
    150181                state = getattr(obj, 'state', None)
    151182                if state and state != VERIFIED:
    152                     return False, _("Attached documents must be verified first.")
     183                    return False, _(
     184                        "Attached documents must be verified first.")
    153185        return True, None
    154186
     
    177209
    178210    grok.implements(
    179         ISampleContractProcess, # must come before ISampleContract
     211        ISampleContractProcess,  # must come before ISampleContract
    180212        ISampleContract,
    181213        ISampleContractEdit,
     
    210242        return implementedBy(SampleContract)
    211243
     244
    212245@grok.subscribe(IContract, grok.IObjectAddedEvent)
    213246def handle_contract_added(contract, event):
     
    218251        IWorkflowInfo(contract).fireTransition('create')
    219252    return
     253
     254
     255@grok.subscribe(IPaymentWaitingForGatewayEvent)
     256def handle_payment_waiting_for_gw(event):
     257    maybe_contract = find_payable_from_payable_id(
     258        event.object.payable_id)
     259    if IContract.providedBy(maybe_contract):
     260        IWorkflowInfo(maybe_contract).fireTransition('await')
     261
     262
     263@grok.subscribe(IPaymentFinishedEvent)
     264def handle_payment_finished(event):
     265    payment = event.object
     266    maybe_contract = find_payable_from_payable_id(payment.payable_id)
     267    if not IContract.providedBy(maybe_contract):
     268        return
     269    if payment.state == STATE_PAID:
     270        IWorkflowInfo(maybe_contract).fireTransition('confirm')
     271    else:
     272        IWorkflowInfo(maybe_contract).fireTransition('discard')
     273
     274
     275class ContractFinder(grok.GlobalUtility):
     276    grok.name('contracts_finder')
     277    grok.implements(IPayableFinder)
     278
     279    def get_payable_by_id(self, contract_id):
     280        catalog = queryUtility(ICatalog, 'contracts_catalog')
     281        if catalog is None:
     282            return None
     283        result = catalog.searchResults(
     284            contract_id=(contract_id, contract_id))
     285        result = [x for x in result]
     286        if not result:
     287            return None
     288        # there should not be more than one result really.
     289        return result[0]
     290
     291
     292class PayableContract(grok.Adapter):
     293    """Adapter to adapt IContracts to IPayable.
     294    """
     295
     296    grok.context(IContract)
     297    grok.implements(IPayable)
     298
     299    def __init__(self, context):
     300        self.context = context
     301        currencies = set([x.currency for x in context.product_options])
     302        if len(currencies) > 1:
     303            raise ValueError(
     304                "Only contracts with same currency for all options allowed.")
     305        return
     306
     307    @property
     308    def payable_id(self):
     309        return self.context.contract_id
     310
     311    @property
     312    def title(self):
     313        return self.context.title or u''
     314
     315    @property
     316    def currency(self):
     317        if not len(self.context.product_options):
     318            return None
     319        return self.context.product_options[0].currency
     320
     321    @property
     322    def payment_items(self):
     323        result = []
     324        for num, option in enumerate(self.context.product_options):
     325            item = PaymentItem()
     326            item.item_id = u'%s' % num
     327            item.title = option.title
     328            item.amount = option.fee
     329            result.append(item)
     330        return tuple(result)
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/customer.py

    r12553 r12741  
    2525from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
    2626from zope.password.interfaces import IPasswordManager
    27 from zope.component import getUtility, createObject
     27from zope.catalog.interfaces import ICatalog
     28from zope.component import getUtility, queryUtility
    2829from zope.component.interfaces import IFactory
    2930from zope.interface import implementedBy
    3031from zope.securitypolicy.interfaces import IPrincipalRoleManager
    31 from zope.schema.interfaces import ConstraintNotSatisfied
    3232
    3333from waeup.ikoba.image import IkobaImageFile
     
    3535from waeup.ikoba.interfaces import (
    3636    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
    37     IIkobaUtils, IExtFileStore,
    38     CREATED, REQUESTED, APPROVED)
     37    IIkobaUtils, IExtFileStore, )
    3938from waeup.ikoba.customers.interfaces import (
    4039    ICustomer, ICustomerNavigation, ICSVCustomerExporter,
     
    4342from waeup.ikoba.customers.documents import CustomerDocumentsContainer
    4443from waeup.ikoba.customers.contracts import ContractsContainer
    45 from waeup.ikoba.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
     44from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder
     45from waeup.ikoba.utils.helpers import (
     46    attrs_to_fields, now, copy_filesystem_tree)
     47
    4648
    4749class Customer(grok.Container):
     
    7779            'password'] = passwordmanager.encodePassword(password)
    7880        self.temp_password['user'] = user
    79         self.temp_password['timestamp'] = datetime.utcnow()  # offset-naive datetime
     81        self.temp_password[
     82            'timestamp'] = datetime.utcnow()  # offset-naive datetime
    8083
    8184    def getTempPassword(self):
     
    8992        if temp_password_dict is not None:
    9093            delta = timedelta(minutes=self.temp_password_minutes)
    91             now = datetime.utcnow()
    92             if now < temp_password_dict.get('timestamp') + delta:
     94            dt_now = datetime.utcnow()
     95            if dt_now < temp_password_dict.get('timestamp') + delta:
    9396                return temp_password_dict.get('password')
    9497            else:
     
    98101
    99102    def writeLogMessage(self, view, message):
    100         ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
     103        ob_class = view.__implemented__.__name__.replace(
     104            'waeup.ikoba.', '')
    101105        self.__parent__.logger.info(
    102106            '%s - %s - %s' % (ob_class, self.__name__, message))
     
    196200    return
    197201
     202
    198203def move_customer_files(customer, del_dir):
    199204    """Move files belonging to `customer` to `del_dir`.
     
    246251        timestamp = str(now().replace(microsecond=0))  # store UTC timestamp
    247252        for num, row in enumerate(csv_data[1:-1]):
    248             csv_data[num+1] = csv_data[num+1] + ',' + timestamp
     253            csv_data[num + 1] = csv_data[num + 1] + ',' + timestamp
    249254        csv_path = os.path.join(del_dir, '%s.csv' % name)
    250255
     
    399404        return file, path, IkobaImageFile(
    400405            file_obj.filename, file_obj.data)
     406
     407
     408class CustomerPayer(grok.Adapter):
     409    """Adapter to turn customers into IPayers.
     410    """
     411    grok.implements(IPayer)
     412    grok.context(ICustomer)
     413
     414    @property
     415    def first_name(self):
     416        return getattr(self.context, 'firstname', None)
     417
     418    @property
     419    def last_name(self):
     420        return getattr(self.context, 'lastname', None)
     421
     422    @property
     423    def payer_id(self):
     424        return getattr(self.context, 'customer_id', None)
     425
     426
     427class CustomerFinder(grok.GlobalUtility):
     428    """Find customers.
     429    """
     430    grok.name('customer_finder')
     431    grok.implements(IPayerFinder)
     432
     433    def get_payer_by_id(self, customer_id):
     434        catalog = queryUtility(ICatalog, 'customers_catalog')
     435        if catalog is None:
     436            return None
     437        result = catalog.searchResults(
     438            customer_id=(customer_id, customer_id))
     439        result = [x for x in result]
     440        if not result:
     441            return None
     442        # there should not be more than one result really.
     443        return result[0]
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/export.py

    r12500 r12741  
    215215                      'translated_class_name',
    216216                      'is_editable',
    217                       'is_approvable'])))
     217                      'is_approvable',
     218                      'customer'])))
    218219
    219220    def filter_func(self, x, **kw):
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py

    r12663 r12741  
    298298    is_approvable = Attribute('Contract approvable by officer')
    299299    translated_class_name = Attribute('Translatable class name')
     300    customer = Attribute('Customer object of context.')
    300301    user_id = Attribute('Id of a user, actually the id of the customer')
    301302    title = Attribute('Title generated by the associated product')
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py

    r12663 r12741  
    11## $Id$
    2 ## 
     2##
    33## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
    44## This program is free software; you can redistribute it and/or modify
     
    66## the Free Software Foundation; either version 2 of the License, or
    77## (at your option) any later version.
    8 ## 
     8##
    99## This program is distributed in the hope that it will be useful,
    1010## but WITHOUT ANY WARRANTY; without even the implied warranty of
    1111## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    1212## GNU General Public License for more details.
    13 ## 
     13##
    1414## You should have received a copy of the GNU General Public License
    1515## along with this program; if not, write to the Free Software
     
    2222import tempfile
    2323import logging
    24 import pytz
    2524import base64
    2625from decimal import Decimal
     
    4241from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
    4342from waeup.ikoba.app import Company
    44 from waeup.ikoba.customers.interfaces import ICustomersUtils
    45 from waeup.ikoba.customers.customer import Customer
    4643from waeup.ikoba.interfaces import (
    4744    IUserAccount, IJobManager, APPROVED, SUBMITTED,
    48     IFileStoreNameChooser, IExtFileStore, IFileStoreHandler, NotIdValue)
    49 from waeup.ikoba.imagestorage import (
    50     FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
    51     DefaultStorage)
    52 from waeup.ikoba.authentication import LocalRoleSetEvent
     45    IFileStoreNameChooser, NotIdValue)
     46from waeup.ikoba.imagestorage import ExtFileStore
    5347from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
    5448from waeup.ikoba.interfaces import VERIFIED
     
    6155SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
    6256SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
     57
    6358
    6459def lookup_submit_value(name, value, browser):
     
    7267            break
    7368    return None
     69
    7470
    7571class CustomersFullSetup(FunctionalTestCase):
     
    119115        prodoption.fee = Decimal('99.9')
    120116        prodoption.currency = 'USD'
    121         self.product.options = [prodoption,]
     117        self.product.options = [prodoption, ]
    122118        self.app['products'].addProduct(self.product)
    123119
     
    138134        self.document.document_id = u'DOC1'
    139135        self.assertRaises(
    140             NotIdValue, setattr, self.document, 'document_id', u'id with spaces')
     136            NotIdValue, setattr, self.document, 'document_id',
     137            u'id with spaces')
    141138        self.customer['documents'].addDocument(self.document)
    142139        self.contract = createObject(self._contract_factory)
    143140        self.contract.contract_id = u'CON1'
    144141        self.assertRaises(
    145             NotIdValue, setattr, self.contract, 'contract_id', u'id with spaces')
     142            NotIdValue, setattr, self.contract, 'contract_id',
     143            u'id with spaces')
    146144        self.customer['contracts'].addContract(self.contract)
    147145
     
    299297        return
    300298
     299
    301300class OfficerUITests(CustomersFullSetup):
    302301    # Tests for Customer class views and pages
    303 
    304302
    305303    def setup_logging(self):
     
    331329        self.browser.getLink("Logout").click()
    332330        self.assertTrue('You have been logged out' in self.browser.contents)
    333         # But we are still logged in since we've used basic authentication here.
    334         # Wikipedia says: Existing browsers retain authentication information
    335         # until the tab or browser is closed or the user clears the history.
    336         # HTTP does not provide a method for a server to direct clients to
    337         # discard these cached credentials. This means that there is no
    338         # effective way for a server to "log out" the user without closing
    339         # the browser. This is a significant defect that requires browser
    340         # manufacturers to support a "logout" user interface element ...
     331        # But we are still logged in since we've used basic
     332        # authentication here.  Wikipedia says: Existing browsers
     333        # retain authentication information until the tab or browser
     334        # is closed or the user clears the history.  HTTP does not
     335        # provide a method for a server to direct clients to discard
     336        # these cached credentials. This means that there is no
     337        # effective way for a server to "log out" the user without
     338        # closing the browser. This is a significant defect that
     339        # requires browser manufacturers to support a "logout" user
     340        # interface element ...
    341341        self.assertTrue('Manager' in self.browser.contents)
    342342
     
    378378        self.browser.open(self.customer_path)
    379379        self.browser.getLink("Send email").click()
    380         self.browser.getControl(name="form.subject").value = 'Important subject'
     380        self.browser.getControl(
     381            name="form.subject").value = 'Important subject'
    381382        self.browser.getControl(name="form.body").value = 'Hello!'
    382383        self.browser.getControl("Send message now").click()
     
    410411        self.assertTrue('passport.jpg deleted' in self.browser.contents)
    411412
    412 
    413413    def test_manage_workflow_send_transition_information(self):
    414414        # Managers can pass through the whole workflow
     
    417417        self.setup_logging()
    418418        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
    419         customer = self.app['customers'][self.customer_id]
     419        self.customer = self.app['customers'][self.customer_id]
    420420        self.browser.open(self.trigtrans_path)
    421421        self.browser.getControl(name="transition").value = ['start']
     
    459459        self.browser.getControl("Apply").click()
    460460        self.browser.open(self.trigtrans_path)
    461         self.browser.getControl(name="transition").value = ['approve_provisionally']
     461        self.browser.getControl(name="transition").value = [
     462            'approve_provisionally']
    462463        self.browser.getControl("Apply").click()
    463464        self.browser.open(self.trigtrans_path)
     
    499500        self.browser.getControl('Perform import').click()
    500501        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
    501         self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
     502        self.assertTrue(
     503            'Successfully processed 2 rows' in self.browser.contents)
    502504        self.assertTrue('Batch processing finished' in self.browser.contents)
    503505
     
    535537        self.browser.getLink("History").click()
    536538        self.assertTrue(
    537             'Customer account deactivated by Manager<br />' in self.browser.contents)
    538         self.assertTrue(
    539             'Customer account activated by Manager<br />' in self.browser.contents)
     539            'Customer account deactivated by Manager<br />'
     540            in self.browser.contents)
     541        self.assertTrue(
     542            'Customer account activated by Manager<br />'
     543            in self.browser.contents)
    540544        # ... and actions have been logged.
    541545        logfile = os.path.join(
    542546            self.app['datacenter'].storage, 'logs', 'customers.log')
    543547        logcontent = open(logfile).read()
    544         self.assertTrue('zope.mgr - customers.browser.CustomerDeactivatePage - '
    545                         'K1000000 - account deactivated' in logcontent)
    546         self.assertTrue('zope.mgr - customers.browser.CustomerActivatePage - '
    547                         'K1000000 - account activated' in logcontent)
    548 
     548        self.assertTrue(
     549            'zope.mgr - customers.browser.CustomerDeactivatePage - '
     550            'K1000000 - account deactivated' in logcontent)
     551        self.assertTrue(
     552            'zope.mgr - customers.browser.CustomerActivatePage - '
     553            'K1000000 - account activated' in logcontent)
    549554
    550555    def test_login_as_customer(self):
     
    554559        self.app['users']['mrofficer'].title = 'Harry Actor'
    555560        prmglobal = IPrincipalRoleManager(self.app)
    556         prmglobal.assignRoleToPrincipal('waeup.CustomerImpersonator', 'mrofficer')
     561        prmglobal.assignRoleToPrincipal(
     562            'waeup.CustomerImpersonator', 'mrofficer')
    557563        prmglobal.assignRoleToPrincipal('waeup.CustomersManager', 'mrofficer')
    558564        self.assertEqual(self.customer.state, 'created')
     
    574580        # We are logged in as customer and can see the 'My Data' tab
    575581        self.assertMatches(
    576             '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
     582            '...<a href="#" class="dropdown-toggle"'
     583            ' data-toggle="dropdown">...',
    577584            self.browser.contents)
    578585        self.assertMatches(
     
    724731        self.assertMatches(
    725732            '...<div class="alert alert-warning">'
    726             'Your account has been deactivated.</div>...', self.browser.contents)
     733            'Your account has been deactivated.</div>...',
     734            self.browser.contents)
    727735        # If suspended_comment is set this message will be flashed instead
    728736        self.customer.suspended_comment = u'Aetsch baetsch!'
     
    778786        self.assertTrue('An email with' in self.browser.contents)
    779787
     788
    780789class CustomerRegistrationTests(CustomersFullSetup):
    781790    # Tests for customer registration
     
    823832            cat.searchResults(
    824833            email=('new@yy.zz', 'new@yy.zz')))
    825         self.assertEqual(self.customer,results[0])
     834        self.assertEqual(self.customer, results[0])
    826835        logfile = os.path.join(
    827836            self.app['datacenter'].storage, 'logs', 'main.log')
    828837        logcontent = open(logfile).read()
    829         self.assertTrue('zope.anybody - customers.browser.CustomerRequestPasswordPage - '
    830                         '123 (K1000000) - new@yy.zz' in logcontent)
     838        self.assertTrue(
     839            'zope.anybody - customers.browser.CustomerRequestPasswordPage - '
     840            '123 (K1000000) - new@yy.zz' in logcontent)
    831841        return
    832842
     
    841851        self.browser.getControl(name="form.email").value = 'newcustomer@xx.zz'
    842852        self.browser.getControl("Send login credentials").click()
    843         self.assertTrue('Your request was successful.' in self.browser.contents)
     853        self.assertTrue(
     854            'Your request was successful.' in self.browser.contents)
    844855        # Customer can be found in the catalog via the email address
    845856        cat = queryUtility(ICatalog, name='customers_catalog')
     
    849860        self.assertEqual(self.app['customers']['K1000001'], results[0])
    850861        self.assertEqual(self.app['customers']['K1000001'].firstname, 'Ruben')
    851         self.assertEqual(self.app['customers']['K1000001'].lastname, 'Gonzales')
     862        self.assertEqual(
     863            self.app['customers']['K1000001'].lastname, 'Gonzales')
    852864        logfile = os.path.join(
    853865            self.app['datacenter'].storage, 'logs', 'main.log')
    854866        logcontent = open(logfile).read()
    855         self.assertTrue('zope.anybody - customers.browser.CustomerCreateAccountPage - '
    856                         'K1000001 - newcustomer@xx.zz' in logcontent)
     867        self.assertTrue(
     868            'zope.anybody - customers.browser.CustomerCreateAccountPage - '
     869            'K1000001 - newcustomer@xx.zz' in logcontent)
    857870        return
     871
    858872
    859873class CustomerDataExportTests(CustomersFullSetup, FunctionalAsyncTestCase):
     
    929943        self.browser.getControl("Add document").click()
    930944        self.assertTrue('PDF Document added.' in self.browser.contents)
    931         docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
     945        docid = [i for i in self.customer['documents'].keys()
     946                 if len(i) > 10][0]
    932947        document = self.customer['documents'][docid]
    933948
     
    950965        self.browser.getControl(name="transition").value = ['verify']
    951966        self.browser.getControl("Apply").click()
    952         self.assertTrue('Customer has not yet been approved' in self.browser.contents)
     967        self.assertTrue(
     968            'Customer has not yet been approved' in self.browser.contents)
    953969        IWorkflowState(self.customer).setState(APPROVED)
    954970        # Document can only be verified if files have been uploaded before
     
    958974        self.assertTrue('No file uploaded' in self.browser.contents)
    959975        self.assertEqual(document.state, 'submitted')
    960         # We set state here manually (verification is tested in test_verify_document)
     976        # We set state here manually (verification is tested in
     977        # test_verify_document)
    961978        IWorkflowState(document).setState(VERIFIED)
    962979
     
    964981        self.browser.open(self.documents_path + '/' + docid + '/index')
    965982        self.assertFalse(
    966             'href="http://localhost/app/customers/K1000000/documents/%s/manage"'
     983            'href="http://localhost/app/customers/K1000000/'
     984            'documents/%s/manage"'
    967985            % docid in self.browser.contents)
    968986        self.browser.open(self.documents_path + '/' + docid + '/manage')
     
    10121030        self.browser.getLink("Documents").click()
    10131031        self.browser.getControl("Add document").click()
    1014         self.assertTrue('The requested form is locked' in self.browser.contents)
     1032        self.assertTrue(
     1033            'The requested form is locked' in self.browser.contents)
    10151034        # Customer is in wrong state
    10161035        IWorkflowState(self.customer).setState(APPROVED)
    10171036        self.browser.getControl("Add document").click()
    1018         self.browser.getControl(name="doctype").value = ['CustomerSampleDocument']
     1037        self.browser.getControl(name="doctype").value = [
     1038            'CustomerSampleDocument']
    10191039        self.browser.getControl(name="form.title").value = 'My Sample Document'
    10201040        self.browser.getControl("Add document").click()
    10211041        self.assertTrue('Sample Document added.' in self.browser.contents)
    1022         docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
     1042        docid = [i for i in self.customer['documents'].keys()
     1043                 if len(i) > 10][0]
    10231044        document = self.customer['documents'][docid]
    10241045        self.browser.getControl(name="form.title").value = 'My second doc'
     
    10381059            name='upload_samplescaneditupload').click()
    10391060        self.assertTrue(
    1040             'href="http://localhost/app/customers/K1000000/documents/%s/sample"'
     1061            'href="http://localhost/app/customers/K1000000/'
     1062            'documents/%s/sample"'
    10411063            % docid in self.browser.contents)
    10421064        # Customer can submit the form. The form is also saved.
     
    10451067        self.assertEqual(document.title, 'My third doc')
    10461068        self.assertEqual(document.state, 'submitted')
    1047         self.assertTrue('Document State: submitted for verification' in self.browser.contents)
     1069        self.assertTrue(
     1070            'Document State: submitted for verification'
     1071            in self.browser.contents)
    10481072        # Customer can't edit the document once it has been submitted
    10491073        self.browser.open(self.documents_path + '/%s/edit' % docid)
    1050         self.assertTrue('The requested form is locked' in self.browser.contents)
     1074        self.assertTrue(
     1075            'The requested form is locked' in self.browser.contents)
    10511076
    10521077    def test_manage_upload_sample_file(self):
     
    10551080        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
    10561081        self.browser.open(self.customer_path + '/documents/DOC1/manage')
    1057         # Create a pseudo image file and select it to be uploaded 
     1082        # Create a pseudo image file and select it to be uploaded
    10581083        image = open(SAMPLE_IMAGE, 'rb')
    10591084        ctrl = self.browser.getControl(name='samplescanmanageupload')
     
    10611086        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
    10621087        # The Save action does not upload files
    1063         self.browser.getControl("Save").click() # submit form
     1088        self.browser.getControl("Save").click()  # submit form
    10641089        self.assertFalse(
    1065             'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
     1090            'href="http://localhost/app/customers/K1000000/'
     1091            'documents/DOC1/sample"'
    10661092            in self.browser.contents)
    10671093        # ... but the correct upload submit button does
     
    10731099            name='upload_samplescanmanageupload').click()
    10741100        self.assertTrue(
    1075             'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"'
     1101            'href="http://localhost/app/customers/K1000000/'
     1102            'documents/DOC1/sample"'
    10761103            in self.browser.contents)
    10771104        # Browsing the link shows a real image
     
    10981125            'Uploaded file is too big' in self.browser.contents)
    10991126        # We do not rely on filename extensions given by uploaders
    1100         image = open(SAMPLE_IMAGE, 'rb') # a jpg-file
     1127        image = open(SAMPLE_IMAGE, 'rb')  # a jpg-file
    11011128        ctrl = self.browser.getControl(name='samplescanmanageupload')
    11021129        file_ctrl = ctrl.mech_control
     
    11521179        self.browser.getControl(name="form.title").value = 'My PDF Document'
    11531180        self.browser.getControl("Add document").click()
    1154         docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
     1181        docid = [
     1182            i for i in self.customer['documents'].keys() if len(i) > 10][0]
    11551183        self.browser.open(self.documents_path + '/%s/manage' % docid)
    11561184        # Create a pseudo image file and select it to be uploaded
     
    11771205            name='upload_pdfscanmanageupload').click()
    11781206        self.assertTrue(
    1179             'href="http://localhost/app/customers/K1000000/documents/%s/sample.pdf">%s.pdf</a>'
     1207            'href="http://localhost/app/customers/K1000000/'
     1208            'documents/%s/sample.pdf">%s.pdf</a>'
    11801209            % (docid, docid[:9]) in self.browser.contents)
    11811210        # Browsing the link shows a real pdf
     
    11941223        self.browser.getLink("Download documents overview").click()
    11951224        self.assertEqual(self.browser.headers['Status'], '200 Ok')
    1196         self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
     1225        self.assertEqual(
     1226            self.browser.headers['Content-Type'], 'application/pdf')
    11971227        path = os.path.join(samples_dir(), 'documents_overview_slip.pdf')
    11981228        open(path, 'wb').write(self.browser.contents)
     
    12001230        # Officers can open document slips which shows a thumbnail of
    12011231        # the jpeg file attached.
    1202         file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample.jpg')
     1232        file_id = IFileStoreNameChooser(self.document).chooseName(
     1233            attr='sample.jpg')
    12031234        fs = ExtFileStore(root=self.dc_root)
    12041235        jpegfile = open(SAMPLE_IMAGE, 'rb')
     
    12071238        self.browser.getLink("Download document slip").click()
    12081239        self.assertEqual(self.browser.headers['Status'], '200 Ok')
    1209         self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
     1240        self.assertEqual(
     1241            self.browser.headers['Content-Type'], 'application/pdf')
    12101242        path = os.path.join(samples_dir(), 'document_slip.pdf')
    12111243        open(path, 'wb').write(self.browser.contents)
     
    12161248        self.customer['documents'].addDocument(pdfdocument)
    12171249        # Add pdf file
    1218         file_id = IFileStoreNameChooser(pdfdocument).chooseName(attr='sample.pdf')
     1250        file_id = IFileStoreNameChooser(pdfdocument).chooseName(
     1251            attr='sample.pdf')
    12191252        fs = ExtFileStore(root=self.dc_root)
    12201253        pdffile = open(SAMPLE_PDF, 'rb')
    12211254        fs.createFile(file_id, pdffile)
    1222         docid = [i for i in self.customer['documents'].keys() if len(i) > 10][0]
     1255        docid = [i for i in self.customer['documents'].keys()
     1256                 if len(i) > 10][0]
    12231257        self.browser.open(self.customer_path + '/documents/' + docid)
    12241258        self.browser.getLink("Download document slip").click()
    12251259        self.assertEqual(self.browser.headers['Status'], '200 Ok')
    1226         self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
     1260        self.assertEqual(
     1261            self.browser.headers['Content-Type'], 'application/pdf')
    12271262        path = os.path.join(samples_dir(), 'pdfdocument_slip.pdf')
    12281263        open(path, 'wb').write(self.browser.contents)
     
    12321267        # A proper file name chooser is registered for customer documents.
    12331268        # This is not a UI test. It's just a functional test.
    1234         file_id = IFileStoreNameChooser(self.document).chooseName(attr='sample')
     1269        file_id = IFileStoreNameChooser(self.document).chooseName(
     1270            attr='sample')
    12351271        fs = ExtFileStore(root=self.dc_root)
    12361272        fs.createFile(file_id, StringIO('my sample 1'))
    12371273        result = fs.getFileByContext(self.document, attr='sample')
    1238         self.assertEqual(file_id, '__file-customerdocument__01000/K1000000/sample_DOC1_K1000000')
     1274        self.assertEqual(
     1275            file_id, '__file-customerdocument__01000/'
     1276            'K1000000/sample_DOC1_K1000000')
    12391277        self.assertEqual(result.read(), 'my sample 1')
    1240         self.assertEqual(self.document.connected_files[0][1].read(), 'my sample 1')
     1278        self.assertEqual(
     1279            self.document.connected_files[0][1].read(), 'my sample 1')
    12411280        self.document.setMD5()
    1242         self.assertEqual(self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24')
     1281        self.assertEqual(
     1282            self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24')
    12431283        return
    12441284
     
    12461286class ContractUITests(CustomersFullSetup):
    12471287    # Tests for contract related views and pages
     1288
     1289    never_ending_button_text = (
     1290        'Select payment method (final submission)')
     1291
     1292    def add_product_option(self, contract):
     1293        prodoption = ProductOption()
     1294        prodoption.title = u'Any product option'
     1295        prodoption.fee = Decimal('88.8')
     1296        prodoption.currency = 'EUR'
     1297        contract.product_options = [prodoption, ]
     1298
     1299    def prepare_payment_select(self):
     1300        IWorkflowState(self.customer).setState('approved')
     1301        IWorkflowState(self.document).setState('verified')
     1302        self.contract.document_object = self.document
     1303        self.add_product_option(self.contract)
     1304        IWorkflowState(self.contract).setState('created')
     1305        # login as customer
     1306        self.browser.open(self.login_path)
     1307        self.browser.getControl(name="form.login").value = self.customer_id
     1308        self.browser.getControl(name="form.password").value = 'cpwd'
     1309        self.browser.getControl("Login").click()
    12481310
    12491311    def test_manage_contract(self):
     
    12611323        self.browser.getControl("Add contract").click()
    12621324        self.assertTrue('Sample Contract added.' in self.browser.contents)
    1263         conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
     1325        conid = [i for i in self.customer['contracts'].keys()
     1326                 if len(i) > 10][0]
    12641327        contract = self.customer['contracts'][conid]
    12651328        self.assertEqual(
    1266             self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
     1329            self.browser.url,
     1330            self.contracts_path + '/%s/selectproduct' % conid)
    12671331        # SAM is in the correct contract_category
    12681332        self.assertTrue('<option value="SAM">' in self.browser.contents)
    12691333        # So far last_product_id is None.
    1270         self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
     1334        self.assertTrue(
     1335            self.customer['contracts'][conid].last_product_id is None)
    12711336        self.browser.getControl(name="form.product_object").value = ['SAM']
    12721337        self.browser.getControl("Save and proceed").click()
     
    12741339            self.browser.url, self.contracts_path + '/%s/manage' % conid)
    12751340        self.browser.getLink("View").click()
    1276         self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
     1341        self.assertEqual(
     1342            self.browser.url, self.contracts_path + '/%s/index' % conid)
    12771343        self.assertEqual(contract.tc_dict, {'en': u'Hello world'})
    12781344
     
    13421408        self.assertFalse('Add contract' in self.browser.contents)
    13431409        self.browser.open(self.contracts_path + '/addcontract')
    1344         self.assertTrue('The requested form is locked' in self.browser.contents)
     1410        self.assertTrue(
     1411            'The requested form is locked' in self.browser.contents)
    13451412        IWorkflowState(self.customer).setState(APPROVED)
    13461413        self.browser.open(self.contracts_path)
     
    13501417        self.browser.getControl("Add contract").click()
    13511418        self.assertTrue('Sample Contract added.' in self.browser.contents)
    1352         conid = [i for i in self.customer['contracts'].keys() if len(i) > 10][0]
     1419        conid = [i for i in self.customer['contracts'].keys()
     1420                 if len(i) > 10][0]
    13531421        contract = self.customer['contracts'][conid]
    13541422        self.assertEqual(
    1355             self.browser.url, self.contracts_path + '/%s/selectproduct' % conid)
     1423            self.browser.url,
     1424            self.contracts_path + '/%s/selectproduct' % conid)
    13561425        # SAM is in the correct contract_category ...
    13571426        self.assertTrue('<option value="SAM">' in self.browser.contents)
     
    13591428        self.assertFalse('<option value="LIC">' in self.browser.contents)
    13601429        # So far last_product_id is None.
    1361         self.assertTrue(self.customer['contracts'][conid].last_product_id is None)
     1430        self.assertTrue(
     1431            self.customer['contracts'][conid].last_product_id is None)
    13621432        self.browser.getControl(name="form.product_object").value = ['SAM']
    13631433        self.browser.getControl("Save and proceed").click()
     
    13661436        # Document is a required field on edit form page.
    13671437        self.browser.getControl("Save").click()
    1368         self.assertTrue('Document: <span class="error">Required input is missing.</span>'
     1438        self.assertTrue(
     1439            'Document: <span class="error">Required input is missing.</span>'
    13691440            in self.browser.contents)
    13701441        # But our document can't be selected because it's not submitted
     
    13761447        # After saving the form, last_product_id and other attributes are set
    13771448        self.assertTrue('Form has been saved.' in self.browser.contents)
    1378         self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
     1449        self.assertEqual(
     1450            self.customer['contracts'][conid].last_product_id, 'SAM')
    13791451        self.assertEqual(contract.title, 'Our Sample Product')
    13801452        self.assertEqual(contract.product_object, self.product)
     
    13821454        # Saving the form again does not unset last_product_id
    13831455        self.browser.getControl("Save").click()
    1384         self.assertEqual(self.customer['contracts'][conid].last_product_id, 'SAM')
     1456        self.assertEqual(
     1457            self.customer['contracts'][conid].last_product_id, 'SAM')
    13851458        self.assertTrue('Form has been saved.' in self.browser.contents)
    13861459        # So far we have not yet set product options.
     
    13901463        prodoption.fee = Decimal('88.8')
    13911464        prodoption.currency = 'EUR'
    1392         contract.product_options = [prodoption,]
     1465        contract.product_options = [prodoption, ]
    13931466        self.browser.open(self.contracts_path + '/%s/edit' % conid)
    13941467        # We can see both the stored and the recent product options
    13951468        # from the chosen product.
    1396         self.assertTrue('<option selected="selected" value="Any product option">'
    1397                         'Any product option @ 88.8 Euro</option>'
    1398                         in self.browser.contents)
     1469        self.assertTrue(
     1470            '<option selected="selected" value="Any product option">'
     1471            'Any product option @ 88.8 Euro</option>'
     1472            in self.browser.contents)
    13991473        self.assertTrue('<option value="First option">First option '
    14001474                        '@ 99.9 US Dollar</option>' in self.browser.contents)
     
    14021476        self.browser.getControl(
    14031477            name="form.product_options.0.").value = ['First option']
    1404         self.assertEqual(contract.product_options[0].title, 'Any product option')
     1478        self.assertEqual(
     1479            contract.product_options[0].title, 'Any product option')
    14051480        self.browser.getControl("Save").click()
    14061481        self.assertEqual(contract.product_options[0].title, 'First option')
    14071482        self.browser.getLink("View").click()
    1408         self.assertTrue('<span>First option @ 99.9 US Dollar</span>' in self.browser.contents)
    1409         self.assertEqual(self.browser.url, self.contracts_path + '/%s/index' % conid)
     1483        self.assertTrue(
     1484            '<span>First option @ 99.9 US Dollar</span>'
     1485            in self.browser.contents)
     1486        self.assertEqual(
     1487            self.browser.url, self.contracts_path + '/%s/index' % conid)
    14101488        # An href attribute is referring to the document and product objects
    14111489        self.assertTrue('<a href="http://localhost/app/products/SAM">SAM -'
    14121490            in self.browser.contents)
    14131491        self.assertTrue(
    1414             '<a href="http://localhost/app/customers/K1000000/documents/DOC1">DOC1 -'
     1492            '<a href="http://localhost/app/customers/K1000000/'
     1493            'documents/DOC1">DOC1 -'
    14151494            in self.browser.contents)
    14161495        # Customer can submit the form if confirmation box is ticket.
     
    14181497        self.browser.getLink("Edit").click()
    14191498        self.browser.getControl("Proceed to checkout").click()
    1420         self.assertTrue('confirm your acceptance of these by ticking' in self.browser.contents)
     1499        self.assertTrue(
     1500            'confirm your acceptance of these by ticking'
     1501            in self.browser.contents)
    14211502        self.assertEqual(contract.state, 'created')
    14221503        self.browser.getControl(name="confirm_tc").value = True
    14231504        self.browser.getControl("Proceed to checkout").click()
    14241505        self.assertEqual(contract.state, 'created')
     1506        radio_ctrl = self.browser.getControl(name='gw')
     1507        radio_ctrl.value = [radio_ctrl.options[0]]  # pick first payment opt
    14251508        self.browser.getControl("Select payment method").click()
    14261509        self.assertEqual(contract.state, 'awaiting')
    14271510        # Customer can't edit the contract once it has been submitted
    14281511        self.browser.open(self.contracts_path + '/%s/edit' % conid)
    1429         self.assertTrue('The requested form is locked' in self.browser.contents)
     1512        self.assertTrue(
     1513            'The requested form is locked' in self.browser.contents)
    14301514
    14311515    def test_view_slips(self):
     
    14351519        self.browser.getLink("Download contracts overview").click()
    14361520        self.assertEqual(self.browser.headers['Status'], '200 Ok')
    1437         self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
     1521        self.assertEqual(
     1522            self.browser.headers['Content-Type'], 'application/pdf')
    14381523        path = os.path.join(samples_dir(), 'contracts_overview_slip.pdf')
    14391524        open(path, 'wb').write(self.browser.contents)
     
    14511536        self.browser.getLink("Download contract slip").click()
    14521537        self.assertEqual(self.browser.headers['Status'], '200 Ok')
    1453         self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
     1538        self.assertEqual(
     1539            self.browser.headers['Content-Type'], 'application/pdf')
    14541540        path = os.path.join(samples_dir(), 'contract_slip.pdf')
    14551541        open(path, 'wb').write(self.browser.contents)
     
    14861572        # InvalidTransitionError is catched
    14871573        self.assertTrue(
    1488             '<div class="alert alert-warning">Attached documents must be verified first.</div>'
     1574            '<div class="alert alert-warning">Attached documents '
     1575            'must be verified first.</div>'
    14891576            in self.browser.contents)
    14901577        self.browser.open(self.contracts_path + '/CON1/trigtrans')
     
    14931580        self.browser.getControl("Apply").click()
    14941581        self.assertEqual(IWorkflowState(self.contract).getState(), 'approved')
     1582
     1583    def test_select_payment(self):
     1584        # select payment
     1585        self.prepare_payment_select()
     1586        self.browser.open('%s/CON1/edit' % self.contracts_path)
     1587        self.browser.getControl("Proceed to checkout").click()
     1588        self.assertTrue(
     1589            "Select payment method" in self.browser.contents)
     1590        self.assertTrue(
     1591            'Credit Card (Demo Payments)' in self.browser.contents)
     1592
     1593    def test_select_payment_no_choice(self):
     1594        # we get warned if no payment was selected
     1595        self.prepare_payment_select()
     1596        self.browser.open(
     1597            '%s/CON1/select_payment_method' % self.contracts_path)
     1598        self.browser.getControl(self.never_ending_button_text).click()
     1599        self.assertTrue(
     1600            'Please pick a payment method' in self.browser.contents)
     1601
     1602    def test_select_payment_demo_provider(self):
     1603        # we can proceed with payments if we select a payment method
     1604        self.prepare_payment_select()
     1605        self.browser.open(
     1606            '%s/CON1/select_payment_method' % self.contracts_path)
     1607        radio_ctrl = self.browser.getControl(name='gw')
     1608        radio_ctrl.displayValue = ['Credit Card (Demo Payments)']
     1609        self.browser.getControl(self.never_ending_button_text).click()
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_contract.py

    r12259 r12741  
    1919Tests for contracts.
    2020"""
     21import decimal
    2122from zope.interface.verify import verifyClass, verifyObject
    22 from zope.component import createObject
     23from zope.component import createObject, getUtility, getUtilitiesFor
     24from zope.component.hooks import setSite
    2325from hurry.workflow.interfaces import (
    2426    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
    2527from waeup.ikoba.customers.interfaces import (
    2628    IContractsContainer, IContract)
    27 from waeup.ikoba.interfaces import IObjectHistory
    2829from waeup.ikoba.customers.contracts import (
    29     ContractsContainer, SampleContract)
     30    ContractsContainer, SampleContract, ContractPayer, ContractFinder,
     31    PayableContract,
     32    )
     33from waeup.ikoba.app import Company
     34from waeup.ikoba.customers.customer import Customer
     35from waeup.ikoba.payments.interfaces import (
     36    IPaymentItem, IPayer, IPayableFinder, IPayable,
     37    )
     38from waeup.ikoba.products.productoptions import ProductOption
    3039from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
     40
    3141
    3242class ContractsContainerTestCase(FunctionalTestCase):
     
    8999        self.assertTrue('Contract created by system' in messages)
    90100        return
     101
     102
     103class TestContractHelpers(FunctionalTestCase):
     104
     105    layer = FunctionalLayer
     106
     107    def test_payer_adapter(self):
     108        # we can adapt IContract to IPayer (i.e. create a payer)
     109        customer = Customer()
     110        customer.firstname, customer.lastname = u'Anna', u'Tester'
     111        contract = createObject(u'waeup.SampleContract')
     112        customer['contracts'] = ContractsContainer()
     113        customer['contracts'].addContract(contract)
     114        result = IPayer(contract)
     115        self.assertTrue(isinstance(result, ContractPayer))
     116        verifyObject(IPayer, result)
     117        self.assertEqual(result.first_name, u'Anna')
     118        self.assertEqual(result.last_name, u'Tester')
     119        self.assertEqual(result.payer_id, customer.customer_id)
     120
     121    def test_contract_finder_iface(self):
     122        # we have a contract finder that returns IPayableFinder data.
     123        verifyClass(IPayableFinder, ContractFinder)
     124
     125    def test_contract_finder_registered(self):
     126        # the contract finder is a utility registered on startup
     127        util = getUtility(IPayableFinder, name='contracts_finder')
     128        self.assertTrue(isinstance(util, ContractFinder))
     129        utils = [util for name, util in getUtilitiesFor(IPayableFinder)
     130                 if isinstance(util, ContractFinder)]
     131        self.assertEqual(len(utils), 1)
     132
     133    def create_contract_and_site(self):
     134        contract = SampleContract()
     135        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "USD")
     136        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
     137        contract.product_options = [option1, option2]
     138        contract.contract_id = u'CON1234'
     139        self.getRootFolder()['app'] = Company()
     140        app = self.getRootFolder()['app']
     141        setSite(app)
     142        return contract, app
     143
     144    def test_contract_finder(self):
     145        # the contract finder can really find contracts
     146        contract, app = self.create_contract_and_site()
     147        app['mycontract'] = contract  # trigger cataloging
     148        finder = ContractFinder()
     149        result = finder.get_payable_by_id('CON1234')
     150        self.assertTrue(result is contract)
     151
     152    def test_contract_finder_not_stored(self):
     153        # we get none if an id is not stored
     154        contract, app = self.create_contract_and_site()
     155        app['mycontract'] = contract  # trigger cataloging
     156        finder = ContractFinder()
     157        result = finder.get_payable_by_id('Not-a-valid-id')
     158        self.assertTrue(result is None)
     159
     160    def test_contract_finder_no_catalog(self):
     161        # contract finder does not complain about missing catalog
     162        finder = ContractFinder()
     163        result = finder.get_payable_by_id('CON1234')
     164        self.assertTrue(result is None)
     165
     166
     167class TestContractAsPayable(FunctionalTestCase):
     168
     169    layer = FunctionalLayer
     170
     171    def test_adaptable(self):
     172        # we can turn contracts into payables.
     173        contract = SampleContract()
     174        payable = IPayable(contract)
     175        self.assertTrue(payable is not None)
     176        self.assertTrue(isinstance(payable, PayableContract))
     177
     178    def test_payable_iface(self):
     179        # PayableContracts really provide IPayable
     180        contract = SampleContract()
     181        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "USD")
     182        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
     183        contract.product_options = [option1, option2]
     184        payable = PayableContract(contract)
     185        verifyObject(IPayable, payable)
     186        verifyClass(IPayable, PayableContract)
     187
     188    def test_payable_simple_attributes(self):
     189        # the simple attribs are set correctly, according to context contract
     190        contract = SampleContract()
     191        contract.title = u'the title'
     192        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
     193        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "EUR")
     194        contract.product_options = [option1, option2]
     195        payable = PayableContract(contract)
     196        self.assertTrue(contract.contract_id, payable.payable_id)
     197        self.assertEqual(payable.title, contract.title)
     198        self.assertEqual(payable.currency, 'EUR')
     199
     200    def test_payable_items(self):
     201        # we can get payment items from payable
     202        contract = SampleContract()
     203        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
     204        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "EUR")
     205        contract.product_options = [option1, option2]
     206        payable = PayableContract(contract)
     207        items = payable.payment_items
     208        self.assertTrue(isinstance(items, tuple))
     209        self.assertEqual(len(items), 2)
     210        verifyObject(IPaymentItem, items[0])
     211        verifyObject(IPaymentItem, items[1])
     212        self.assertEqual(items[0].item_id, '0')
     213        self.assertEqual(items[0].title, u'Fee 1')
     214        self.assertEqual(items[0].amount, decimal.Decimal("31.10"))
     215
     216    def test_payable_no_items(self):
     217        # payables work also with no options set on contract
     218        contract = SampleContract()
     219        payable = PayableContract(contract)
     220        items = payable.payment_items
     221        self.assertTrue(isinstance(items, tuple))
     222        self.assertEqual(len(items), 0)
     223        self.assertEqual(payable.currency, None)
     224
     225    def test_different_currencies_forbiddedn(self):
     226        # we do not accept different currencies in payment items
     227        contract = SampleContract()
     228        option1 = ProductOption(u"Fee 1", decimal.Decimal("31.10"), "EUR")
     229        option2 = ProductOption(u"Fee 2", decimal.Decimal("12.12"), "USD")
     230        contract.product_options = [option1, option2]
     231        self.assertRaises(ValueError, PayableContract, contract)
  • main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_customer.py

    r12297 r12741  
    2121import re
    2222import unittest
    23 import grok
    2423from cStringIO import StringIO
    25 from datetime import tzinfo
    26 from zope.component import getUtility, queryUtility, createObject
    27 from zope.catalog.interfaces import ICatalog
     24from zope.component import getUtility, getUtilitiesFor
     25from zope.component.hooks import setSite
    2826from zope.component.interfaces import IFactory
    29 from zope.event import notify
    3027from zope.interface import verify
    31 from zope.schema.interfaces import RequiredMissing
    3228from waeup.ikoba.interfaces import IExtFileStore, IFileStoreNameChooser
     29from waeup.ikoba.app import Company
    3330from waeup.ikoba.customers.customer import (
    34     Customer, CustomerFactory, handle_customer_removed, path_from_custid)
     31    Customer, CustomerFactory, handle_customer_removed, path_from_custid,
     32    CustomerPayer, CustomerFinder,
     33    )
    3534from waeup.ikoba.customers.interfaces import (
    3635    ICustomer, ICustomerNavigation, ICustomersUtils)
    3736from waeup.ikoba.customers.tests.test_batching import CustomerImportExportSetup
     37from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder
    3838from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase
     39
    3940
    4041class HelperTests(unittest.TestCase):
     
    5960            path_from_custid('KM123456'), u'00120/KM123456')
    6061        return
     62
    6163
    6264class CustomerTest(FunctionalTestCase):
     
    9294
    9395
     96class TestCustomerHelpers(FunctionalTestCase):
     97
     98    layer = FunctionalLayer
     99
     100    def test_payer_adapter(self):
     101        # we can adapt ICustomer to IPayer (i.e. create a payer)
     102        customer = Customer()
     103        customer.firstname, customer.lastname = u'Anna', u'Tester'
     104        result = IPayer(customer)
     105        self.assertTrue(isinstance(result, CustomerPayer))
     106        verify.verifyObject(IPayer, result)
     107        self.assertEqual(result.first_name, u'Anna')
     108        self.assertEqual(result.last_name, u'Tester')
     109        self.assertEqual(result.payer_id, customer.customer_id)
     110
     111    def test_customer_finder_iface(self):
     112        # we have a customer finder that returns IPayableFinder data.
     113        verify.verifyClass(IPayerFinder, CustomerFinder)
     114
     115    def test_customer_finder_registered(self):
     116        # the customer finder is a utility registered on startup
     117        util = getUtility(IPayerFinder, name='customer_finder')
     118        self.assertTrue(isinstance(util, CustomerFinder))
     119        utils = [util for name, util in getUtilitiesFor(IPayerFinder)
     120                 if isinstance(util, CustomerFinder)]
     121        self.assertEqual(len(utils), 1)
     122
     123    def create_customer_and_site(self):
     124        customer = Customer()
     125        customer.customer_id = u'CUST1'
     126        self.getRootFolder()['app'] = Company()
     127        app = self.getRootFolder()['app']
     128        setSite(app)
     129        return customer, app
     130
     131    def test_customer_finder(self):
     132        # the customer finder can really find customers
     133        customer, app = self.create_customer_and_site()
     134        app['mycustomer'] = customer  # trigger cataloging
     135        finder = CustomerFinder()
     136        result = finder.get_payer_by_id('CUST1')
     137        self.assertTrue(result is customer)
     138
     139    def test_customer_finder_not_stored(self):
     140        # we get none if an id is not stored
     141        customer, app = self.create_customer_and_site()
     142        app['mycustomer'] = customer  # trigger cataloging
     143        finder = CustomerFinder()
     144        result = finder.get_payer_by_id('Not-a-valid-id')
     145        self.assertTrue(result is None)
     146
     147    def test_customer_finder_no_catalog(self):
     148        # customer finder does not complain about missing catalog
     149        finder = CustomerFinder()
     150        result = finder.get_payer_by_id('CUST1')
     151        self.assertTrue(result is None)
     152
     153
    94154class CustomerRemovalTests(CustomerImportExportSetup):
    95155    # Test handle_customer_removed
     
    192252        return
    193253
     254
    194255class CustomerFactoryTest(FunctionalTestCase):
    195256
  • 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.