Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/README-paypal.rst
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/README-paypal.rst	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/README-paypal.rst	(revision 12060)
@@ -0,0 +1,68 @@
+Support for PayPal(tm) Payments
+*******************************
+
+Ikoba generally supports paying things via PayPal. For this, we use
+the REST-API provided by PayPal.
+
+To make that work, you have to (1) obtain app credentials from PayPal
+and (2) make that credentials known to ikoba.
+
+
+Getting Credentials from PayPal
++++++++++++++++++++++++++++++++
+
+1) Create a general PayPal account
+
+   Browse https://www.paypal.com and create a new account. If you plan
+   to provide services or goods payed via PayPal. you should of course
+   create a business account. You can, however, start with a personal
+   account as it is upgradeable at a later point in time.
+
+   For only testing ikoba PayPal support, a personal account is
+   sufficient.
+
+2) Register as a developer at PayPal
+
+   Go to https://developer.paypal.com and log in with the credentials
+   obtained in step 1. This will create some fake users/merchants for
+   you, which are *not* suitable for use with the PayPal REST API.
+
+3) Create a REST-API compatible application
+
+   After logging in on https://developer.paypal.com/ go to the
+   dashboard and create a new app by clicking the respective button.
+
+Afterwards, in the developer dashboard under "My apps" you can find
+your new set of client IDs and secrets for both, sandbox and live use.
+
+
+Setting PayPal Client ID, secret, etc.
+++++++++++++++++++++++++++++++++++++++
+
+To make the credentials known to Ikoba, put them into configuration
+files like this::
+
+  # paypal.conf
+
+  [rest-client]
+  id = <YOUR-VERY-OWN-PAYPAL-REST-CLIENT-ID>
+  secret = <YOUR-VERY-OWN-PAYPAL-REST-CLIENT-SECRET>
+  # valid values: sandbox, live
+  mode = sandbox
+
+Out of the box, we expect these files to be in the following _two_
+locations:
+
+  path/to/ikoba/install/etc/paypal.conf
+  path/to/ikoba/install/etc/paypal-testing.conf
+
+Obviously, the second file is used for (automated) testing.
+
+.. note:: As these credentials are bound to
+          persons/companies/organisations, we do not share them via
+          version control.
+
+For developers: The credentials given via ZCML and config files are
+used on startup to initialize the PayPal SDK. Therefore, it should not
+be neccessary to care for the credentials when creating Payment
+objects after the Grok instance started.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/paypal.py	(revision 12060)
@@ -0,0 +1,137 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Support for PayPal payments.
+"""
+import ConfigParser
+import paypalrestsdk
+from zope.component import queryUtility
+from waeup.ikoba.interfaces import IPayPalConfig
+
+
+def parse_paypal_config(path):
+    """Get paypal credentials from config file.
+
+    Returns a dict with following keys set:
+
+      ``client_id``, ``client_secret``, ``mode``.
+
+    Please note, that values might be `None`.
+    """
+    parser = ConfigParser.SafeConfigParser(
+        {'id': None, 'secret': None, 'mode': 'sandbox', }
+        )
+    parser.readfp(open(path))
+    return {
+        'client_id': parser.get('rest-client', 'id', None),
+        'client_secret': parser.get('rest-client', 'secret', None),
+        'mode': parser.get('rest-client', 'mode', 'sandbox')
+        }
+
+
+def get_paypal_config_file_path():
+    """Get the path of the configuration file (if any).
+
+    If no such file is registered, we get `None`.
+    """
+    util = queryUtility(IPayPalConfig)
+    if util is None:
+        return None
+    return util['path']
+
+
+def configure_sdk(path):
+    """Configure the PayPal SDK with values from config in `path`.
+
+    Parse paypal configuration from file in `path` and set
+    configuration in SDK accordingly.
+
+    Returns a dict with values from config file and path set.
+
+    This function will normally be called during start-up and only
+    this one time.
+
+    It is neccessary to authorize later calls to other part of the
+    PayPal API.
+    """
+    conf = parse_paypal_config(path)
+    paypalrestsdk.configure({
+        'mode': conf['mode'],
+        'client_id': conf['client_id'],
+        'client_secret': conf['client_secret'],
+        })
+    conf['path'] = path
+    return conf
+
+
+def get_access_token():
+    """Get an access token for further calls.
+    """
+    conf = parse_paypal_config(get_paypal_config_file_path())
+    api = paypalrestsdk.set_config(
+        mode=conf['mode'],  # sandbox or live
+        client_id=conf['client_id'],
+        client_secret=conf['client_secret'],
+        )
+    return api.get_access_token()
+
+
+def get_payment():
+    """Construct a payment.
+
+    You have to `create()` the payment yourself.
+
+    XXX: Just some sampledata yet.
+    """
+    payment = paypalrestsdk.Payment(
+        {
+            "intent": "sale",
+            "payer": {
+                "payment_method": "credit_card",
+                "funding_instruments": [
+                    {
+                        "credit_card": {
+                            "type": "visa",
+                            "number": "4417119669820331",
+                            "expire_month": "11",
+                            "expire_year": "2018",
+                            "cvv2": "874",
+                            "first_name": "Joe",
+                            "last_name": "Shopper",
+                            "billing_address": {
+                                "line1": "52 N Main ST",
+                                "city": "Johnstown",
+                                "state": "OH",
+                                "postal_code": "43210",
+                                "country_code": "US"}
+                            }
+                        }
+                    ]},
+            "transactions": [{
+                    "amount": {
+                        "total": "7.47",
+                        "currency": "USD",
+                        "details": {
+                            "subtotal": "7.41",
+                            "tax": "0.03",
+                            "shipping": "0.03"}},
+                    "description": ("This is the payment "
+                                    "transaction description.")
+                    }]
+            }
+        )
+    return payment
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests.py	(revision 11962)
+++ 	(revision )
@@ -1,58 +1,0 @@
-## $Id$
-##
-## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
-## This program is free software; you can redistribute it and/or modify
-## it under the terms of the GNU General Public License as published by
-## the Free Software Foundation; either version 2 of the License, or
-## (at your option) any later version.
-##
-## This program is distributed in the hope that it will be useful,
-## but WITHOUT ANY WARRANTY; without even the implied warranty of
-## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-## GNU General Public License for more details.
-##
-## You should have received a copy of the GNU General Public License
-## along with this program; if not, write to the Free Software
-## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-##
-"""
-Tests for payments.
-"""
-from zope.interface.verify import verifyClass, verifyObject
-from waeup.ikoba.payments.interfaces import (
-    IPaymentsContainer, IOnlinePayment)
-from waeup.ikoba.payments.container import PaymentsContainer
-from waeup.ikoba.payments.payment import OnlinePayment
-from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
-
-class PaymentsContainerTestCase(FunctionalTestCase):
-
-    layer = FunctionalLayer
-
-    def test_interfaces(self):
-        # Make sure the correct interfaces are implemented.
-        self.assertTrue(
-            verifyClass(
-                IPaymentsContainer, PaymentsContainer)
-            )
-        self.assertTrue(
-            verifyObject(
-                IPaymentsContainer, PaymentsContainer())
-            )
-        self.assertTrue(
-            verifyClass(
-                IOnlinePayment, OnlinePayment)
-            )
-        self.assertTrue(
-            verifyObject(
-                IOnlinePayment, OnlinePayment())
-            )
-        return
-
-    def test_base(self):
-        # We cannot call the fundamental methods of a base in that case
-        container = PaymentsContainer()
-        self.assertRaises(
-            NotImplementedError, container.archive)
-        self.assertRaises(
-            NotImplementedError, container.clear)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/__init__.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/__init__.py	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/__init__.py	(revision 12060)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_interfaces.py	(revision 12060)
@@ -0,0 +1,59 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Tests for payments.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.ikoba.payments.interfaces import (
+    IPaymentsContainer, IOnlinePayment)
+from waeup.ikoba.payments.container import PaymentsContainer
+from waeup.ikoba.payments.payment import OnlinePayment
+from waeup.ikoba.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class PaymentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IPaymentsContainer, PaymentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPaymentsContainer, PaymentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IOnlinePayment, OnlinePayment)
+            )
+        self.assertTrue(
+            verifyObject(
+                IOnlinePayment, OnlinePayment())
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = PaymentsContainer()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
Index: main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py
===================================================================
--- main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12060)
+++ main/waeup.ikoba/trunk/src/waeup/ikoba/payments/tests/test_paypal.py	(revision 12060)
@@ -0,0 +1,137 @@
+## $Id$
+##
+## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import os
+import shutil
+import tempfile
+import unittest
+import paypalrestsdk
+from zope.component import getGlobalSiteManager, queryUtility
+from waeup.ikoba.interfaces import IPayPalConfig
+from waeup.ikoba.payments.paypal import (
+    get_paypal_config_file_path, parse_paypal_config, get_access_token,
+    configure_sdk, get_payment,
+    )
+from waeup.ikoba.testing import (
+    FunctionalLayer, FunctionalTestCase,
+    )
+
+#
+# PayPal test config
+#
+
+EXTERNAL_PAYPAL_TESTS = False
+
+#
+# End of PayPal test config
+#
+
+
+def external_paypal_test(func):
+    """A decorator that can block test functions.
+    """
+    if not EXTERNAL_PAYPAL_TESTS:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-2]
+        print "WARNING: external paypal tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+
+class HelperTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        # unregister any remaining utils registered during tests
+        util = queryUtility(IPayPalConfig)
+        if util is not None:
+            getGlobalSiteManager().unregisterUtility(util, IPayPalConfig)
+
+    def test_get_paypal_config_file_path(self):
+        # we can get a config file path (if registered)
+        path = os.path.join(self.workdir, 'sample.cfg')
+        getGlobalSiteManager().registerUtility(
+            {'path': path, }, IPayPalConfig, '')
+        result = get_paypal_config_file_path()
+        assert result == path
+
+    def test_get_paypal_config_file_path_no_file(self):
+        # without IPayPalConfig util set, we get `None`
+        result = get_paypal_config_file_path()
+        assert result is None
+
+    def test_parse_paypal_config(self):
+        # we can parse paypal configs
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            "id = myid\n"
+            "secret = mysecret\n"
+            "mode = live\n")
+        result = parse_paypal_config(path)
+        assert result['client_id'] == u'myid'
+        assert result['client_secret'] == u'mysecret'
+        assert result['mode'] == 'live'
+
+    def test_parse_paypal_config_defaults(self):
+        # we have fallback values (defaults) in place
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            )
+        result = parse_paypal_config(path)
+        assert result['client_id'] is None
+        assert result['client_secret'] is None
+        assert result['mode'] == "sandbox"
+
+    def test_configure_sdk(self):
+        # we can configure the paypal sdk
+        path = os.path.join(self.workdir, 'sample.cfg')
+        open(path, 'w').write(
+            "[rest-client]\n"
+            "id = my-special-id\n"
+            )
+        result = configure_sdk(path)
+        assert result['client_id'] == 'my-special-id'
+        assert result['client_secret'] is None
+        assert result['mode'] == "sandbox"
+        assert paypalrestsdk.api.__api__.mode == 'sandbox'
+        assert paypalrestsdk.api.__api__.client_id == 'my-special-id'
+
+
+class FunctionalPaypalTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    @external_paypal_test
+    def test_get_access_token(self):
+        # we can get an access token
+        result = get_access_token()
+        assert isinstance(result, unicode)
+
+    @external_paypal_test
+    def test_get_payment(self):
+        # we can construct (paypal-)payment objects
+        payment = get_payment()
+        assert isinstance(payment, paypalrestsdk.Payment)
+        result = payment.create()
+        assert result is True
