Changeset 12741 for main/waeup.ikoba/trunk/src/waeup/ikoba/payments
- Timestamp:
- 12 Mar 2015, 05:29:43 (10 years ago)
- Location:
- main/waeup.ikoba/trunk/src/waeup/ikoba/payments
- Files:
-
- 7 edited
- 1 copied
Legend:
- Unmodified
- Added
- Removed
-
main/waeup.ikoba/trunk/src/waeup/ikoba/payments/demo_provider.py
r12671 r12741 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.