Changeset 12741 for main/waeup.ikoba/trunk/src/waeup/ikoba
- Timestamp:
- 12 Mar 2015, 05:29:43 (10 years ago)
- 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 35 35 from waeup.ikoba.products.container import ProductsContainer 36 36 from waeup.ikoba.documents.container import DocumentsContainer 37 from waeup.ikoba.payments.container import PaymentsContainer 37 38 38 39 … … 59 60 the like. 60 61 """ 61 62 62 self['users'] = UsersContainer() 63 63 self['datacenter'] = DataCenter() … … 67 67 self['products'] = ProductsContainer() 68 68 self['documents'] = DocumentsContainer() 69 self['payments'] = PaymentsContainer() 69 70 self._createPlugins() 70 71 -
main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser.py
r12663 r12741 18 18 """UI components for customers and related components. 19 19 """ 20 21 import sys22 20 import grok 23 import pytz24 21 import os 25 22 from urllib import urlencode 26 from datetime import datetime27 23 from zope.event import notify 28 24 from zope.i18n import translate 29 25 from zope.catalog.interfaces import ICatalog 30 26 from zope.component import queryUtility, getUtility, createObject 31 from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing32 from zope.formlib.textwidgets import BytesDisplayWidget33 27 from zope.security import checkPermission 34 28 from hurry.workflow.interfaces import ( … … 42 36 from waeup.ikoba.browser.layout import ( 43 37 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) 48 39 from waeup.ikoba.browser.pages import ContactAdminForm 49 40 from waeup.ikoba.browser.breadcrumbs import Breadcrumb 50 41 from waeup.ikoba.browser.interfaces import ICaptchaManager 51 42 from waeup.ikoba.mandates.mandate import PasswordMandate 43 from waeup.ikoba.payments.payment import format_payment_item_values 44 from waeup.ikoba.payments.interfaces import ( 45 IPaymentGatewayServicesLister, IPaymentGatewayService, IPayer, IPayable 46 ) 52 47 from waeup.ikoba.widgets.hrefwidget import HREFDisplayWidget 53 48 from waeup.ikoba.utils.helpers import ( 54 get_current_principal, to_timezone, now,format_date)49 get_current_principal, format_date) 55 50 from waeup.ikoba.customers.interfaces import ( 56 51 ICustomer, ICustomersContainer, ICustomerRequestPW, ICustomersUtils, 57 52 ICustomerDocument, ICustomerDocumentsContainer, ICustomerCreate, 58 53 ICustomerPDFDocument, IContractsContainer, IContract, 59 IContractSelectProduct, ISampleContract,54 IContractSelectProduct, 60 55 ) 61 56 from waeup.ikoba.customers.catalog import search 62 57 from waeup.ikoba.customers.workflow import PAYMENT_TRANSITIONS 58 63 59 64 60 grok.context(IIkobaObject) … … 927 923 def createDocument(self, **data): 928 924 form = self.request.form 929 customer = self.context.__parent__930 925 doctype = form.get('doctype', None) 931 926 # Here we can create various instances of CustomerDocument derived … … 1098 1093 tableheader = [] 1099 1094 tabledata = [] 1100 contenttitle = []1101 1095 for i in range(1,3): 1102 1096 tabledata.append(sorted( … … 1134 1128 @property 1135 1129 def label(self): 1136 portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE1137 1130 return '%s of %s\nTitle: %s' % ( 1138 1131 self.context.translated_class_name, … … 1141 1134 1142 1135 def render(self): 1143 portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE1144 1136 customerview = CustomerBasePDFFormPage(self.context.customer, 1145 1137 self.request, self.omit_fields) … … 1157 1149 1158 1150 def render(self): 1159 portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE1160 1151 customerview = CustomerBasePDFFormPage(self.context.customer, 1161 1152 self.request, self.omit_fields) … … 1295 1286 def createContract(self, **data): 1296 1287 form = self.request.form 1297 customer = self.context.__parent__1298 1288 contype = form.get('contype', None) 1299 1289 # Here we can create various instances of Contract derived … … 1506 1496 label = _('Select payment method') 1507 1497 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): 1509 1510 if self.context.state != CREATED or not self.context.fee_based: 1510 1511 emit_lock_message(self) 1511 1512 return 1513 self.gw = gw 1512 1514 super(SelectPaymentMethodPage, self).update() 1513 1515 return 1514 1516 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') 1517 1519 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 1521 1543 1522 1544 class ContractTriggerTransitionFormPage(IkobaEditFormPage): … … 1600 1622 tableheader = [] 1601 1623 tabledata = [] 1602 contenttitle = []1603 1624 for i in range(1,3): 1604 1625 tabledata.append(sorted( … … 1655 1676 @property 1656 1677 def label(self): 1657 portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE1658 1678 return self.context.title 1659 1679 … … 1670 1690 1671 1691 def render(self): 1672 portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE1673 1692 customerview = CustomerBasePDFFormPage(self.context.customer, 1674 1693 self.request, self.omit_fields) -
main/waeup.ikoba/trunk/src/waeup/ikoba/customers/browser_templates/selectpaymentmethodpage.pt
r12663 r12741 1 1 2 <form action="." tal:attributes="action request/URL" method="post" 2 3 i18n:domain="waeup.ikoba" enctype="multipart/form-data"> 3 4 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"> 14 6 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 19 16 <br /> 20 17 -
main/waeup.ikoba/trunk/src/waeup/ikoba/customers/contracts.py
r12663 r12741 19 19 Customer contract components. 20 20 """ 21 import os22 21 import grok 23 from zope.component import queryUtility, getUtility 22 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState 23 from zope.catalog.interfaces import ICatalog 24 from zope.component import getUtility, queryUtility 24 25 from zope.component.interfaces import IFactory 25 26 from zope.interface import implementedBy 26 27 from zope.schema import getFields 27 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState28 28 from waeup.ikoba.interfaces import MessageFactory as _ 29 29 from waeup.ikoba.interfaces import ( 30 IIkobaUtils, IObjectHistory, 31 VERIFIED, APPROVED, PROVISIONALLY, 32 IIDSource) 30 IObjectHistory, VERIFIED, APPROVED, PROVISIONALLY, IIDSource) 33 31 from 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) 35 from waeup.ikoba.payments.interfaces import ( 36 IPayer, IPayableFinder, IPayable, IPaymentWaitingForGatewayEvent, 37 STATE_PAID, STATE_FAILED, IPaymentFinishedEvent 38 ) 39 from waeup.ikoba.payments.payment import ( 40 PaymentItem, find_payable_from_payable_id, 41 ) 39 42 from waeup.ikoba.utils.helpers import attrs_to_fields 43 40 44 41 45 class ContractsContainer(grok.Container): … … 61 65 ContractsContainer = attrs_to_fields(ContractsContainer) 62 66 67 68 class 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 63 94 class ContractBase(grok.Container): 64 95 """This is a customer contract baseclass. 65 96 """ 66 grok.implements(IContractSelectProduct) # Nec esary for the selectproduct page67 97 grok.implements(IContractSelectProduct) # Neccessary for the 98 # selectproduct page (why?) 68 99 grok.baseclass() 69 100 … … 150 181 state = getattr(obj, 'state', None) 151 182 if state and state != VERIFIED: 152 return False, _("Attached documents must be verified first.") 183 return False, _( 184 "Attached documents must be verified first.") 153 185 return True, None 154 186 … … 177 209 178 210 grok.implements( 179 ISampleContractProcess, # must come before ISampleContract211 ISampleContractProcess, # must come before ISampleContract 180 212 ISampleContract, 181 213 ISampleContractEdit, … … 210 242 return implementedBy(SampleContract) 211 243 244 212 245 @grok.subscribe(IContract, grok.IObjectAddedEvent) 213 246 def handle_contract_added(contract, event): … … 218 251 IWorkflowInfo(contract).fireTransition('create') 219 252 return 253 254 255 @grok.subscribe(IPaymentWaitingForGatewayEvent) 256 def 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) 264 def 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 275 class 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 292 class 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 25 25 from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo 26 26 from zope.password.interfaces import IPasswordManager 27 from zope.component import getUtility, createObject 27 from zope.catalog.interfaces import ICatalog 28 from zope.component import getUtility, queryUtility 28 29 from zope.component.interfaces import IFactory 29 30 from zope.interface import implementedBy 30 31 from zope.securitypolicy.interfaces import IPrincipalRoleManager 31 from zope.schema.interfaces import ConstraintNotSatisfied32 32 33 33 from waeup.ikoba.image import IkobaImageFile … … 35 35 from waeup.ikoba.interfaces import ( 36 36 IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler, 37 IIkobaUtils, IExtFileStore, 38 CREATED, REQUESTED, APPROVED) 37 IIkobaUtils, IExtFileStore, ) 39 38 from waeup.ikoba.customers.interfaces import ( 40 39 ICustomer, ICustomerNavigation, ICSVCustomerExporter, … … 43 42 from waeup.ikoba.customers.documents import CustomerDocumentsContainer 44 43 from waeup.ikoba.customers.contracts import ContractsContainer 45 from waeup.ikoba.utils.helpers import attrs_to_fields, now, copy_filesystem_tree 44 from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder 45 from waeup.ikoba.utils.helpers import ( 46 attrs_to_fields, now, copy_filesystem_tree) 47 46 48 47 49 class Customer(grok.Container): … … 77 79 'password'] = passwordmanager.encodePassword(password) 78 80 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 80 83 81 84 def getTempPassword(self): … … 89 92 if temp_password_dict is not None: 90 93 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: 93 96 return temp_password_dict.get('password') 94 97 else: … … 98 101 99 102 def writeLogMessage(self, view, message): 100 ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','') 103 ob_class = view.__implemented__.__name__.replace( 104 'waeup.ikoba.', '') 101 105 self.__parent__.logger.info( 102 106 '%s - %s - %s' % (ob_class, self.__name__, message)) … … 196 200 return 197 201 202 198 203 def move_customer_files(customer, del_dir): 199 204 """Move files belonging to `customer` to `del_dir`. … … 246 251 timestamp = str(now().replace(microsecond=0)) # store UTC timestamp 247 252 for num, row in enumerate(csv_data[1:-1]): 248 csv_data[num +1] = csv_data[num+1] + ',' + timestamp253 csv_data[num + 1] = csv_data[num + 1] + ',' + timestamp 249 254 csv_path = os.path.join(del_dir, '%s.csv' % name) 250 255 … … 399 404 return file, path, IkobaImageFile( 400 405 file_obj.filename, file_obj.data) 406 407 408 class 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 427 class 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 215 215 'translated_class_name', 216 216 'is_editable', 217 'is_approvable']))) 217 'is_approvable', 218 'customer']))) 218 219 219 220 def filter_func(self, x, **kw): -
main/waeup.ikoba/trunk/src/waeup/ikoba/customers/interfaces.py
r12663 r12741 298 298 is_approvable = Attribute('Contract approvable by officer') 299 299 translated_class_name = Attribute('Translatable class name') 300 customer = Attribute('Customer object of context.') 300 301 user_id = Attribute('Id of a user, actually the id of the customer') 301 302 title = Attribute('Title generated by the associated product') -
main/waeup.ikoba/trunk/src/waeup/ikoba/customers/tests/test_browser.py
r12663 r12741 1 1 ## $Id$ 2 ## 2 ## 3 3 ## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann 4 4 ## This program is free software; you can redistribute it and/or modify … … 6 6 ## the Free Software Foundation; either version 2 of the License, or 7 7 ## (at your option) any later version. 8 ## 8 ## 9 9 ## This program is distributed in the hope that it will be useful, 10 10 ## but WITHOUT ANY WARRANTY; without even the implied warranty of 11 11 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 12 ## GNU General Public License for more details. 13 ## 13 ## 14 14 ## You should have received a copy of the GNU General Public License 15 15 ## along with this program; if not, write to the Free Software … … 22 22 import tempfile 23 23 import logging 24 import pytz25 24 import base64 26 25 from decimal import Decimal … … 42 41 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase 43 42 from waeup.ikoba.app import Company 44 from waeup.ikoba.customers.interfaces import ICustomersUtils45 from waeup.ikoba.customers.customer import Customer46 43 from waeup.ikoba.interfaces import ( 47 44 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) 46 from waeup.ikoba.imagestorage import ExtFileStore 53 47 from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase 54 48 from waeup.ikoba.interfaces import VERIFIED … … 61 55 SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp') 62 56 SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf') 57 63 58 64 59 def lookup_submit_value(name, value, browser): … … 72 67 break 73 68 return None 69 74 70 75 71 class CustomersFullSetup(FunctionalTestCase): … … 119 115 prodoption.fee = Decimal('99.9') 120 116 prodoption.currency = 'USD' 121 self.product.options = [prodoption, ]117 self.product.options = [prodoption, ] 122 118 self.app['products'].addProduct(self.product) 123 119 … … 138 134 self.document.document_id = u'DOC1' 139 135 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') 141 138 self.customer['documents'].addDocument(self.document) 142 139 self.contract = createObject(self._contract_factory) 143 140 self.contract.contract_id = u'CON1' 144 141 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') 146 144 self.customer['contracts'].addContract(self.contract) 147 145 … … 299 297 return 300 298 299 301 300 class OfficerUITests(CustomersFullSetup): 302 301 # Tests for Customer class views and pages 303 304 302 305 303 def setup_logging(self): … … 331 329 self.browser.getLink("Logout").click() 332 330 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 ... 341 341 self.assertTrue('Manager' in self.browser.contents) 342 342 … … 378 378 self.browser.open(self.customer_path) 379 379 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' 381 382 self.browser.getControl(name="form.body").value = 'Hello!' 382 383 self.browser.getControl("Send message now").click() … … 410 411 self.assertTrue('passport.jpg deleted' in self.browser.contents) 411 412 412 413 413 def test_manage_workflow_send_transition_information(self): 414 414 # Managers can pass through the whole workflow … … 417 417 self.setup_logging() 418 418 self.browser.addHeader('Authorization', 'Basic mgr:mgrpw') 419 customer = self.app['customers'][self.customer_id]419 self.customer = self.app['customers'][self.customer_id] 420 420 self.browser.open(self.trigtrans_path) 421 421 self.browser.getControl(name="transition").value = ['start'] … … 459 459 self.browser.getControl("Apply").click() 460 460 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'] 462 463 self.browser.getControl("Apply").click() 463 464 self.browser.open(self.trigtrans_path) … … 499 500 self.browser.getControl('Perform import').click() 500 501 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) 502 504 self.assertTrue('Batch processing finished' in self.browser.contents) 503 505 … … 535 537 self.browser.getLink("History").click() 536 538 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) 540 544 # ... and actions have been logged. 541 545 logfile = os.path.join( 542 546 self.app['datacenter'].storage, 'logs', 'customers.log') 543 547 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) 549 554 550 555 def test_login_as_customer(self): … … 554 559 self.app['users']['mrofficer'].title = 'Harry Actor' 555 560 prmglobal = IPrincipalRoleManager(self.app) 556 prmglobal.assignRoleToPrincipal('waeup.CustomerImpersonator', 'mrofficer') 561 prmglobal.assignRoleToPrincipal( 562 'waeup.CustomerImpersonator', 'mrofficer') 557 563 prmglobal.assignRoleToPrincipal('waeup.CustomersManager', 'mrofficer') 558 564 self.assertEqual(self.customer.state, 'created') … … 574 580 # We are logged in as customer and can see the 'My Data' tab 575 581 self.assertMatches( 576 '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...', 582 '...<a href="#" class="dropdown-toggle"' 583 ' data-toggle="dropdown">...', 577 584 self.browser.contents) 578 585 self.assertMatches( … … 724 731 self.assertMatches( 725 732 '...<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) 727 735 # If suspended_comment is set this message will be flashed instead 728 736 self.customer.suspended_comment = u'Aetsch baetsch!' … … 778 786 self.assertTrue('An email with' in self.browser.contents) 779 787 788 780 789 class CustomerRegistrationTests(CustomersFullSetup): 781 790 # Tests for customer registration … … 823 832 cat.searchResults( 824 833 email=('new@yy.zz', 'new@yy.zz'))) 825 self.assertEqual(self.customer, results[0])834 self.assertEqual(self.customer, results[0]) 826 835 logfile = os.path.join( 827 836 self.app['datacenter'].storage, 'logs', 'main.log') 828 837 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) 831 841 return 832 842 … … 841 851 self.browser.getControl(name="form.email").value = 'newcustomer@xx.zz' 842 852 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) 844 855 # Customer can be found in the catalog via the email address 845 856 cat = queryUtility(ICatalog, name='customers_catalog') … … 849 860 self.assertEqual(self.app['customers']['K1000001'], results[0]) 850 861 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') 852 864 logfile = os.path.join( 853 865 self.app['datacenter'].storage, 'logs', 'main.log') 854 866 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) 857 870 return 871 858 872 859 873 class CustomerDataExportTests(CustomersFullSetup, FunctionalAsyncTestCase): … … 929 943 self.browser.getControl("Add document").click() 930 944 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] 932 947 document = self.customer['documents'][docid] 933 948 … … 950 965 self.browser.getControl(name="transition").value = ['verify'] 951 966 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) 953 969 IWorkflowState(self.customer).setState(APPROVED) 954 970 # Document can only be verified if files have been uploaded before … … 958 974 self.assertTrue('No file uploaded' in self.browser.contents) 959 975 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) 961 978 IWorkflowState(document).setState(VERIFIED) 962 979 … … 964 981 self.browser.open(self.documents_path + '/' + docid + '/index') 965 982 self.assertFalse( 966 'href="http://localhost/app/customers/K1000000/documents/%s/manage"' 983 'href="http://localhost/app/customers/K1000000/' 984 'documents/%s/manage"' 967 985 % docid in self.browser.contents) 968 986 self.browser.open(self.documents_path + '/' + docid + '/manage') … … 1012 1030 self.browser.getLink("Documents").click() 1013 1031 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) 1015 1034 # Customer is in wrong state 1016 1035 IWorkflowState(self.customer).setState(APPROVED) 1017 1036 self.browser.getControl("Add document").click() 1018 self.browser.getControl(name="doctype").value = ['CustomerSampleDocument'] 1037 self.browser.getControl(name="doctype").value = [ 1038 'CustomerSampleDocument'] 1019 1039 self.browser.getControl(name="form.title").value = 'My Sample Document' 1020 1040 self.browser.getControl("Add document").click() 1021 1041 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] 1023 1044 document = self.customer['documents'][docid] 1024 1045 self.browser.getControl(name="form.title").value = 'My second doc' … … 1038 1059 name='upload_samplescaneditupload').click() 1039 1060 self.assertTrue( 1040 'href="http://localhost/app/customers/K1000000/documents/%s/sample"' 1061 'href="http://localhost/app/customers/K1000000/' 1062 'documents/%s/sample"' 1041 1063 % docid in self.browser.contents) 1042 1064 # Customer can submit the form. The form is also saved. … … 1045 1067 self.assertEqual(document.title, 'My third doc') 1046 1068 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) 1048 1072 # Customer can't edit the document once it has been submitted 1049 1073 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) 1051 1076 1052 1077 def test_manage_upload_sample_file(self): … … 1055 1080 self.browser.addHeader('Authorization', 'Basic mgr:mgrpw') 1056 1081 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 1058 1083 image = open(SAMPLE_IMAGE, 'rb') 1059 1084 ctrl = self.browser.getControl(name='samplescanmanageupload') … … 1061 1086 file_ctrl.add_file(image, filename='my_sample_scan.jpg') 1062 1087 # The Save action does not upload files 1063 self.browser.getControl("Save").click() # submit form1088 self.browser.getControl("Save").click() # submit form 1064 1089 self.assertFalse( 1065 'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"' 1090 'href="http://localhost/app/customers/K1000000/' 1091 'documents/DOC1/sample"' 1066 1092 in self.browser.contents) 1067 1093 # ... but the correct upload submit button does … … 1073 1099 name='upload_samplescanmanageupload').click() 1074 1100 self.assertTrue( 1075 'href="http://localhost/app/customers/K1000000/documents/DOC1/sample"' 1101 'href="http://localhost/app/customers/K1000000/' 1102 'documents/DOC1/sample"' 1076 1103 in self.browser.contents) 1077 1104 # Browsing the link shows a real image … … 1098 1125 'Uploaded file is too big' in self.browser.contents) 1099 1126 # We do not rely on filename extensions given by uploaders 1100 image = open(SAMPLE_IMAGE, 'rb') # a jpg-file1127 image = open(SAMPLE_IMAGE, 'rb') # a jpg-file 1101 1128 ctrl = self.browser.getControl(name='samplescanmanageupload') 1102 1129 file_ctrl = ctrl.mech_control … … 1152 1179 self.browser.getControl(name="form.title").value = 'My PDF Document' 1153 1180 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] 1155 1183 self.browser.open(self.documents_path + '/%s/manage' % docid) 1156 1184 # Create a pseudo image file and select it to be uploaded … … 1177 1205 name='upload_pdfscanmanageupload').click() 1178 1206 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>' 1180 1209 % (docid, docid[:9]) in self.browser.contents) 1181 1210 # Browsing the link shows a real pdf … … 1194 1223 self.browser.getLink("Download documents overview").click() 1195 1224 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') 1197 1227 path = os.path.join(samples_dir(), 'documents_overview_slip.pdf') 1198 1228 open(path, 'wb').write(self.browser.contents) … … 1200 1230 # Officers can open document slips which shows a thumbnail of 1201 1231 # 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') 1203 1234 fs = ExtFileStore(root=self.dc_root) 1204 1235 jpegfile = open(SAMPLE_IMAGE, 'rb') … … 1207 1238 self.browser.getLink("Download document slip").click() 1208 1239 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') 1210 1242 path = os.path.join(samples_dir(), 'document_slip.pdf') 1211 1243 open(path, 'wb').write(self.browser.contents) … … 1216 1248 self.customer['documents'].addDocument(pdfdocument) 1217 1249 # Add pdf file 1218 file_id = IFileStoreNameChooser(pdfdocument).chooseName(attr='sample.pdf') 1250 file_id = IFileStoreNameChooser(pdfdocument).chooseName( 1251 attr='sample.pdf') 1219 1252 fs = ExtFileStore(root=self.dc_root) 1220 1253 pdffile = open(SAMPLE_PDF, 'rb') 1221 1254 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] 1223 1257 self.browser.open(self.customer_path + '/documents/' + docid) 1224 1258 self.browser.getLink("Download document slip").click() 1225 1259 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') 1227 1262 path = os.path.join(samples_dir(), 'pdfdocument_slip.pdf') 1228 1263 open(path, 'wb').write(self.browser.contents) … … 1232 1267 # A proper file name chooser is registered for customer documents. 1233 1268 # 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') 1235 1271 fs = ExtFileStore(root=self.dc_root) 1236 1272 fs.createFile(file_id, StringIO('my sample 1')) 1237 1273 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') 1239 1277 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') 1241 1280 self.document.setMD5() 1242 self.assertEqual(self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24') 1281 self.assertEqual( 1282 self.document.sample_md5, 'a406995ee8eb6772bacf51aa4b0caa24') 1243 1283 return 1244 1284 … … 1246 1286 class ContractUITests(CustomersFullSetup): 1247 1287 # 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() 1248 1310 1249 1311 def test_manage_contract(self): … … 1261 1323 self.browser.getControl("Add contract").click() 1262 1324 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] 1264 1327 contract = self.customer['contracts'][conid] 1265 1328 self.assertEqual( 1266 self.browser.url, self.contracts_path + '/%s/selectproduct' % conid) 1329 self.browser.url, 1330 self.contracts_path + '/%s/selectproduct' % conid) 1267 1331 # SAM is in the correct contract_category 1268 1332 self.assertTrue('<option value="SAM">' in self.browser.contents) 1269 1333 # 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) 1271 1336 self.browser.getControl(name="form.product_object").value = ['SAM'] 1272 1337 self.browser.getControl("Save and proceed").click() … … 1274 1339 self.browser.url, self.contracts_path + '/%s/manage' % conid) 1275 1340 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) 1277 1343 self.assertEqual(contract.tc_dict, {'en': u'Hello world'}) 1278 1344 … … 1342 1408 self.assertFalse('Add contract' in self.browser.contents) 1343 1409 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) 1345 1412 IWorkflowState(self.customer).setState(APPROVED) 1346 1413 self.browser.open(self.contracts_path) … … 1350 1417 self.browser.getControl("Add contract").click() 1351 1418 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] 1353 1421 contract = self.customer['contracts'][conid] 1354 1422 self.assertEqual( 1355 self.browser.url, self.contracts_path + '/%s/selectproduct' % conid) 1423 self.browser.url, 1424 self.contracts_path + '/%s/selectproduct' % conid) 1356 1425 # SAM is in the correct contract_category ... 1357 1426 self.assertTrue('<option value="SAM">' in self.browser.contents) … … 1359 1428 self.assertFalse('<option value="LIC">' in self.browser.contents) 1360 1429 # 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) 1362 1432 self.browser.getControl(name="form.product_object").value = ['SAM'] 1363 1433 self.browser.getControl("Save and proceed").click() … … 1366 1436 # Document is a required field on edit form page. 1367 1437 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>' 1369 1440 in self.browser.contents) 1370 1441 # But our document can't be selected because it's not submitted … … 1376 1447 # After saving the form, last_product_id and other attributes are set 1377 1448 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') 1379 1451 self.assertEqual(contract.title, 'Our Sample Product') 1380 1452 self.assertEqual(contract.product_object, self.product) … … 1382 1454 # Saving the form again does not unset last_product_id 1383 1455 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') 1385 1458 self.assertTrue('Form has been saved.' in self.browser.contents) 1386 1459 # So far we have not yet set product options. … … 1390 1463 prodoption.fee = Decimal('88.8') 1391 1464 prodoption.currency = 'EUR' 1392 contract.product_options = [prodoption, ]1465 contract.product_options = [prodoption, ] 1393 1466 self.browser.open(self.contracts_path + '/%s/edit' % conid) 1394 1467 # We can see both the stored and the recent product options 1395 1468 # 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) 1399 1473 self.assertTrue('<option value="First option">First option ' 1400 1474 '@ 99.9 US Dollar</option>' in self.browser.contents) … … 1402 1476 self.browser.getControl( 1403 1477 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') 1405 1480 self.browser.getControl("Save").click() 1406 1481 self.assertEqual(contract.product_options[0].title, 'First option') 1407 1482 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) 1410 1488 # An href attribute is referring to the document and product objects 1411 1489 self.assertTrue('<a href="http://localhost/app/products/SAM">SAM -' 1412 1490 in self.browser.contents) 1413 1491 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 -' 1415 1494 in self.browser.contents) 1416 1495 # Customer can submit the form if confirmation box is ticket. … … 1418 1497 self.browser.getLink("Edit").click() 1419 1498 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) 1421 1502 self.assertEqual(contract.state, 'created') 1422 1503 self.browser.getControl(name="confirm_tc").value = True 1423 1504 self.browser.getControl("Proceed to checkout").click() 1424 1505 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 1425 1508 self.browser.getControl("Select payment method").click() 1426 1509 self.assertEqual(contract.state, 'awaiting') 1427 1510 # Customer can't edit the contract once it has been submitted 1428 1511 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) 1430 1514 1431 1515 def test_view_slips(self): … … 1435 1519 self.browser.getLink("Download contracts overview").click() 1436 1520 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') 1438 1523 path = os.path.join(samples_dir(), 'contracts_overview_slip.pdf') 1439 1524 open(path, 'wb').write(self.browser.contents) … … 1451 1536 self.browser.getLink("Download contract slip").click() 1452 1537 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') 1454 1540 path = os.path.join(samples_dir(), 'contract_slip.pdf') 1455 1541 open(path, 'wb').write(self.browser.contents) … … 1486 1572 # InvalidTransitionError is catched 1487 1573 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>' 1489 1576 in self.browser.contents) 1490 1577 self.browser.open(self.contracts_path + '/CON1/trigtrans') … … 1493 1580 self.browser.getControl("Apply").click() 1494 1581 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 19 19 Tests for contracts. 20 20 """ 21 import decimal 21 22 from zope.interface.verify import verifyClass, verifyObject 22 from zope.component import createObject 23 from zope.component import createObject, getUtility, getUtilitiesFor 24 from zope.component.hooks import setSite 23 25 from hurry.workflow.interfaces import ( 24 26 IWorkflowInfo, IWorkflowState, InvalidTransitionError) 25 27 from waeup.ikoba.customers.interfaces import ( 26 28 IContractsContainer, IContract) 27 from waeup.ikoba.interfaces import IObjectHistory28 29 from waeup.ikoba.customers.contracts import ( 29 ContractsContainer, SampleContract) 30 ContractsContainer, SampleContract, ContractPayer, ContractFinder, 31 PayableContract, 32 ) 33 from waeup.ikoba.app import Company 34 from waeup.ikoba.customers.customer import Customer 35 from waeup.ikoba.payments.interfaces import ( 36 IPaymentItem, IPayer, IPayableFinder, IPayable, 37 ) 38 from waeup.ikoba.products.productoptions import ProductOption 30 39 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase) 40 31 41 32 42 class ContractsContainerTestCase(FunctionalTestCase): … … 89 99 self.assertTrue('Contract created by system' in messages) 90 100 return 101 102 103 class 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 167 class 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 21 21 import re 22 22 import unittest 23 import grok24 23 from cStringIO import StringIO 25 from datetime import tzinfo 26 from zope.component import getUtility, queryUtility, createObject 27 from zope.catalog.interfaces import ICatalog 24 from zope.component import getUtility, getUtilitiesFor 25 from zope.component.hooks import setSite 28 26 from zope.component.interfaces import IFactory 29 from zope.event import notify30 27 from zope.interface import verify 31 from zope.schema.interfaces import RequiredMissing32 28 from waeup.ikoba.interfaces import IExtFileStore, IFileStoreNameChooser 29 from waeup.ikoba.app import Company 33 30 from 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 ) 35 34 from waeup.ikoba.customers.interfaces import ( 36 35 ICustomer, ICustomerNavigation, ICustomersUtils) 37 36 from waeup.ikoba.customers.tests.test_batching import CustomerImportExportSetup 37 from waeup.ikoba.payments.interfaces import IPayer, IPayerFinder 38 38 from waeup.ikoba.testing import FunctionalLayer, FunctionalTestCase 39 39 40 40 41 class HelperTests(unittest.TestCase): … … 59 60 path_from_custid('KM123456'), u'00120/KM123456') 60 61 return 62 61 63 62 64 class CustomerTest(FunctionalTestCase): … … 92 94 93 95 96 class 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 94 154 class CustomerRemovalTests(CustomerImportExportSetup): 95 155 # Test handle_customer_removed … … 192 252 return 193 253 254 194 255 class CustomerFactoryTest(FunctionalTestCase): 195 256 -
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py
r12671 r12741 1 import datetime 1 2 import grok 3 import random 4 import re 5 from zope.event import notify 2 6 from waeup.ikoba.interfaces import MessageFactory as _ 3 from waeup.ikoba.payments.interfaces import IPaymentGatewayService 4 from waeup.ikoba.payments.payment import Payment 7 from waeup.ikoba.browser.layout import IkobaEditFormPage, action 8 from waeup.ikoba.payments.interfaces import ( 9 IPaymentGatewayService, IPayment, IPayable, IPayer, IPayee, 10 STATE_AWAITING_GATEWAY_CONFIRM, STATE_PAID) 11 from 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) 5 15 6 16 7 class DemoCreditcardPaymentService(grok.GlobalUtility): 17 grok.templatedir('templates') 18 19 20 WARN_FINAL_SUBMIT = _( 21 'You can not edit your contract after final submission. ' 22 'Do you really want to submit?' 23 ) 24 25 26 RE_CC_NUMBER = re.compile('^[0-9]{9,25}$') 27 RE_CSC = re.compile('^[0-9]{3,4}$') 28 29 30 class DemoCreditcardPaymentService(PaymentProviderServiceBase): 8 31 """A demo payment gateway service. 9 32 … … 13 36 grok.name('demo_creditcard') 14 37 15 title = _(u' Demo Creditcard')38 title = _(u'Credit Card (Demo Payments)') 16 39 17 def create_payment(self, payer, pay ment_item_list=[], payee=None):40 def create_payment(self, payer, payable, payee=None): 18 41 """Create a payment. 19 42 """ 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) 21 50 payment.gateway_service = 'demo_creditcard' # must be grok.name above 22 payment.payer_id = payer.payer_id23 for item in payment_item_list:24 payment.add_payment_item(item)25 51 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 66 class 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 148 class 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 = " " 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 20 20 from zope import schema 21 21 from zope.component import getUtilitiesFor 22 from zope.container.interfaces import IContainer 23 from zope.container.constraints import contains 22 from zope.component.interfaces import IObjectEvent 24 23 from zope.interface import Interface, Attribute 25 24 from waeup.ikoba.interfaces import ( … … 30 29 #: Possible states of payments 31 30 STATE_UNPAID = 1 32 STATE_PAID = 2 33 STATE_FAILED = 4 31 STATE_AWAITING_USER_CONFIRM = 2 32 STATE_AWAITING_GATEWAY_CONFIRM = 4 33 STATE_PAID = 64 34 STATE_FAILED = 128 34 35 35 36 payment_states = SimpleIkobaVocabulary( 36 37 (_('Not yet paid'), STATE_UNPAID), 38 (_('Waiting for user confirm'), STATE_AWAITING_USER_CONFIRM), 39 (_('Waiting for verification'), STATE_AWAITING_GATEWAY_CONFIRM), 37 40 (_('Paid'), STATE_PAID), 38 41 (_('Failed'), STATE_FAILED), … … 40 43 41 44 45 class IPaymentWaitingForGatewayEvent(IObjectEvent): 46 """Fired when a payment starts waiting for verification. 47 """ 48 object = Attribute("""The payment waiting.""") 49 50 51 class IPaymentFinishedEvent(IObjectEvent): 52 """Fired when a payment failed or succeeded. 53 """ 54 object = Attribute("""The payment finished.""") 55 56 57 class IPaymentAborted(IObjectEvent): 58 """Fired when a payment was aborted before external transactions. 59 """ 60 object = Attribute("""The payment aborted.""") 61 62 42 63 class PaymentGatewayServicesSource(BasicSourceFactory): 43 64 """A source that lists available payment services. … … 88 109 ) 89 110 90 def create_payment(payer, pay ment_item_list,payee):111 def create_payment(payer, payable, payee): 91 112 """Create a payment. 92 113 93 114 For all parameters we expect an object, that implements 94 `IPayer`, `IPay mentItem`, or `IPayee` respectively. If not,115 `IPayer`, `IPayable`, or `IPayee` respectively. If not, 95 116 then the given objects must be at least adaptable to the 96 117 respective interface. … … 102 123 """ 103 124 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 104 135 105 136 class IPaymentGatewayServicesLister(Interface): … … 136 167 required=True, 137 168 ) 169 170 171 class 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 190 class 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 """ 138 206 139 207 … … 161 229 162 230 163 class IPayment(IContainer): 231 class 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 277 class IPayment(IIkobaObject): 164 278 """A base representation of payments. 165 279 … … 185 299 we mark the payment 'failed'. 186 300 """ 187 contains(IPaymentItem)188 189 301 payment_id = schema.TextLine( 190 302 title=u'Payment Identifier', 191 303 default=None, 304 required=True, 305 ) 306 307 title = schema.TextLine( 308 title=u'Payment description.', 309 default=u'', 192 310 required=True, 193 311 ) … … 203 321 default=None, 204 322 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 ) 206 330 207 331 gateway_service = schema.Choice( … … 256 380 """ 257 381 258 def add_payment_item(item):259 """Payments contain payment items.260 261 Add one262 """263 264 382 265 383 class IPayer(Interface): -
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/payment.py
r12671 r12741 23 23 import uuid 24 24 from datetime import datetime 25 from zope.component import getUtilitiesFor 25 from zope.catalog.interfaces import ICatalog 26 from zope.component import getUtilitiesFor, getUtility 26 27 from zope.event import notify 28 from waeup.ikoba.interfaces import MessageFactory as _ 27 29 from waeup.ikoba.utils.helpers import attrs_to_fields 30 from waeup.ikoba.utils.logger import Logger 28 31 from waeup.ikoba.payments.interfaces import ( 29 32 IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID, 30 33 IPaymentGatewayService, IPayer, IPaymentItem, IPayee, 31 IPaymentGatewayServicesLister, 34 IPaymentGatewayServicesLister, IPayableFinder, IPayerFinder, 35 IPaymentWaitingForGatewayEvent, IPaymentFinishedEvent, 32 36 ) 33 from waeup.ikoba.utils.logger import Logger 37 38 39 def 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 47 def 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 60 def 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 73 def 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 86 def 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 34 105 35 106 … … 42 113 43 114 115 class PaymentWaitingForGatewayEvent(object): 116 grok.implements(IPaymentWaitingForGatewayEvent) 117 118 def __init__(self, obj): 119 self.object = obj 120 121 122 class PaymentFinishedEvent(object): 123 grok.implements(IPaymentFinishedEvent) 124 125 def __init__(self, obj): 126 self.object = obj 127 128 44 129 class PaymentGatewayServicesLister(grok.GlobalUtility): 45 130 grok.implements(IPaymentGatewayServicesLister) … … 52 137 53 138 class PaymentProviderServiceBase(grok.GlobalUtility): 54 139 """Base for IPaymentGatewayServices. 140 """ 55 141 grok.baseclass() 56 142 grok.implements(IPaymentGatewayService) … … 58 144 title = u'Sample Credit Card Service' 59 145 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 157 class Payment(grok.Model, Logger): 63 158 """This is a payment. 64 159 """ … … 70 165 logger_format_str = '"%(asctime)s","%(user)s",%(message)s' 71 166 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): 84 168 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 85 175 self.creation_date = datetime.utcnow() 86 176 self.payment_date = None 87 177 self.payment_id = u'PAY_' + unicode(uuid.uuid4().hex) 88 178 self.state = STATE_UNPAID 179 self.currency = payable.currency 180 if payee is not None: 181 self.payee_id = payee.payee_id 89 182 return 90 183 … … 113 206 notify(grok.ObjectModifiedEvent(self)) 114 207 115 def add_payment_item(self, item):116 """Add `item`117 118 Returns the key under which the `item` was stored. Please do119 not make anby assumptions about the key. It will be a120 string. That is all we can tell.121 122 """123 cnt = 0124 while str(cnt) in self:125 cnt += 1126 self[str(cnt)] = item127 return str(cnt)128 129 208 130 209 @attrs_to_fields … … 138 217 139 218 @attrs_to_fields 140 class PaymentItem( grok.Model):219 class PaymentItem(object): 141 220 142 221 grok.implements(IPaymentItem) 143 222 144 def __init__(self): 223 def __init__( 224 self, item_id=u"0", title=u"", amount=decimal.Decimal("0.00")): 145 225 super(PaymentItem, self).__init__() 226 self.item_id = item_id 227 self.title = title 228 self.amount = amount 146 229 147 230 -
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py
r12498 r12741 35 35 IPayment, IPaymentGatewayService, IPayer, IPaymentItem, IPayee, 36 36 ) 37 from waeup.ikoba.payments.payment import PaymentProviderServiceBase 37 38 from waeup.ikoba.payments.paypal_countries import COUNTRIES_VOCAB 38 39 from waeup.ikoba.payments.paypal_currencies import CURRENCIES_VOCAB … … 1141 1142 1142 1143 1143 class PayPalCreditCardService( grok.GlobalUtility):1144 class PayPalCreditCardService(PaymentProviderServiceBase): 1144 1145 grok.implements(IPaymentGatewayService) 1145 1146 grok.name('paypal_creditcard') … … 1216 1217 return payment 1217 1218 1218 1219 class PayPalRegularPaymentService(grok.GlobalUtility): 1219 def next_step(self, payment_id): 1220 raise NotImplemented("next_steo() not implemented") 1221 1222 1223 class PayPalRegularPaymentService(PaymentProviderServiceBase): 1220 1224 grok.implements(IPaymentGatewayService) 1221 1225 grok.name('paypal_regular') -
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_demo_provider.py
r12671 r12741 1 import decimal 1 import unittest 2 from zope.component import queryUtility 3 from zope.component.hooks import setSite 2 4 from zope.interface.verify import verifyClass, verifyObject 3 5 from waeup.ikoba.testing import ( … … 7 9 IPaymentGatewayService, IPayment, STATE_UNPAID, 8 10 ) 9 from waeup.ikoba.payments.payment import Payer, PaymentItem, Payee 11 from waeup.ikoba.app import Company 12 from waeup.ikoba.payments.payment import Payer, Payee, Payment 10 13 from waeup.ikoba.payments.demo_provider import ( 11 DemoCreditcardPaymentService, 14 DemoCreditcardPaymentService, RE_CC_NUMBER, RE_CSC, 12 15 ) 16 from waeup.ikoba.payments.tests.test_payment import FakePayer, FakePayable 13 17 14 18 15 class DemoCreditcarPaymentServiceTests(FunctionalTestCase): 19 class 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 38 class DemoCreditcardPaymentServiceTests(FunctionalTestCase): 16 39 17 40 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) 18 47 19 48 def test_iface(self): … … 26 55 # we can get payments from payment gateways 27 56 service = DemoCreditcardPaymentService() 28 payer, pay ment_item, payee = Payer(), PaymentItem(), Payee()57 payer, payable, payee = Payer(), FakePayable(), Payee() 29 58 payer.payer_id = u'SOME_PAYER_ID' 30 result = service.create_payment(payer, [], payee)59 result = service.create_payment(payer, payable, payee) 31 60 assert IPayment.providedBy(result) 32 61 assert result.gateway_service == u'demo_creditcard' 33 62 assert result.state == STATE_UNPAID 34 assert len(result) == 0 # no items stored35 36 def test_create_payment_honors_payment_item(self):37 # we inspect payment items and take their values38 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) == 146 63 47 64 def test_create_payment_honors_payer(self): 48 65 # we inspect payers when creating their payments 49 66 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() 52 68 payer.payer_id = u'SOME_PAYER_ID' 53 result = service.create_payment(payer, [payment_item], payee)69 result = service.create_payment(payer, payable, payee) 54 70 assert result.payer_id == payer.payer_id 55 71 56 72 def test_get_service_by_utility_name(self): 57 73 # we can get the demo credit card service by its utility name 58 from zope.component import queryUtility59 74 service = queryUtility( 60 75 IPaymentGatewayService, name="demo_creditcard") 61 76 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 -*- 1 2 ## $Id$ 2 3 ## … … 20 21 import re 21 22 import unittest 22 from zope.component import getUtilitiesFor, getSiteManager, queryUtility 23 from zope.interface import implements 23 from zope.component import ( 24 getUtilitiesFor, getSiteManager, queryUtility, getGlobalSiteManager, 25 ) 26 from zope.component.hooks import setSite 27 from zope.interface import implements, implementer 24 28 from zope.interface.verify import verifyClass, verifyObject 25 29 from waeup.ikoba.payments.interfaces import ( 26 30 IPayment, STATE_UNPAID, STATE_PAID, STATE_FAILED, 27 31 IPaymentGatewayService, IPaymentItem, IPaymentGatewayServicesLister, 32 IPayableFinder, IPayerFinder, IPayable, IPayer, 28 33 ) 34 from waeup.ikoba.app import Company 29 35 from 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, 31 39 ) 32 40 from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase) 41 42 43 @implementer(IPayer) 44 class 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 53 FAKE_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) 60 class 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 33 74 34 75 … … 61 102 assert result['some_name'] is fake_util 62 103 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 63 133 64 134 class FunctionalHelperTests(FunctionalTestCase): … … 76 146 assert len(util()) > 0 77 147 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 219 class PaymentTests(FunctionalTestCase): 220 221 layer = FunctionalLayer 222 223 def setUp(self): 224 super(PaymentTests, self).setUp() 225 self.payer = FakePayer() 226 self.payable = FakePayable() 80 227 81 228 def test_iface(self): 82 229 # Payments fullfill any interface contracts 83 obj = Payment( )230 obj = Payment(self.payer, self.payable) 84 231 verifyClass(IPayment, Payment) 85 232 verifyObject(IPayment, obj) 86 233 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 87 250 def test_payment_id_unique(self): 88 251 # we get unique payment ids 89 p1, p2 = Payment(), Payment() 252 p1 = Payment(self.payer, self.payable) 253 p2 = Payment(self.payer, self.payable) 90 254 id1, id2 = p1.payment_id, p2.payment_id 91 255 assert id1 != id2 … … 93 257 def test_payment_id_format(self): 94 258 # payment ids have a special format: "PAY_<32 hex digits>" 95 id1 = Payment( ).payment_id259 id1 = Payment(self.payer, self.payable).payment_id 96 260 assert isinstance(id1, basestring) 97 261 assert re.match('PAY_[0-9a-f]{32}', id1) … … 99 263 def test_initial_state_is_unpaid(self): 100 264 # the initial state of payments is <unpaid> 101 p1 = Payment( )265 p1 = Payment(self.payer, self.payable) 102 266 assert p1.state == STATE_UNPAID 103 267 104 268 def test_approve(self): 105 269 # we can approve payments 106 p1 = Payment( )270 p1 = Payment(self.payer, self.payable) 107 271 p1.approve() 108 272 assert p1.state == STATE_PAID … … 112 276 def test_approve_datetime_given(self): 113 277 # we can give a datetime 114 p1 = Payment( )278 p1 = Payment(self.payer, self.payable) 115 279 some_datetime = datetime.datetime(2014, 1, 1, 0, 0, 0) 116 280 p1.approve(payment_date=some_datetime) … … 120 284 # if we do not give a datetime, current one will be used 121 285 current = datetime.datetime.utcnow() 122 p1 = Payment( )286 p1 = Payment(self.payer, self.payable) 123 287 p1.approve() 124 288 assert p1.payment_date >= current … … 126 290 def test_mark_failed(self): 127 291 # we can mark payments as failed 128 p1 = Payment( )292 p1 = Payment(self.payer, self.payable) 129 293 p1.mark_failed() 130 294 assert p1.state == STATE_FAILED 131 295 132 def test_add_payment_item(self):133 # we can add payment items134 p1 = Payment()135 item1 = PaymentItem()136 result = p1.add_payment_item(item1)137 assert len(p1) == 1 # do not make assumptions about result content138 assert isinstance(result, basestring)139 140 def test_add_payment_item_multiple(self):141 # we can add several items142 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 content148 assert isinstance(result1, basestring)149 assert isinstance(result2, basestring)150 151 296 def test_amount(self): 152 297 # 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) 160 302 assert p1.amount == decimal.Decimal("12.75") 161 303 162 304 def test_amount_negative(self): 163 305 # 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) 171 310 assert p1.amount == decimal.Decimal("-1.02") 172 311 173 312 def test_amount_empty(self): 174 # the amount of zero items is 0.00.175 p 1 = 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")) 178 317 179 318 -
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py
r12498 r12741 1174 1174 def test_store_credit_card_invalid(self): 1175 1175 # an exception is raised with invalid credit cards. 1176 s ite = self.create_site()1176 self.create_site() 1177 1177 service = PayPalCreditCardService() 1178 1178 credit_card = CreditCard( … … 1203 1203 # we can actually create payments 1204 1204 service = PayPalCreditCardService() 1205 s ite = self.create_site()1205 self.create_site() 1206 1206 credit_card = self.get_credit_card() 1207 1207 result = service.store_credit_card(credit_card)
Note: See TracChangeset for help on using the changeset viewer.