diff --git a/README.rst b/README.rst index 1bb0fac..960eb47 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Python library for the PayPlug API +Python library for the Payplug API ================================== .. image:: https://github.com/payplug/payplug-python/workflows/CI/badge.svg @@ -9,15 +9,15 @@ Python library for the PayPlug API :target: https://pypi.python.org/pypi/payplug/ :alt: PyPi -This is the documentation of PayPlug's Python library. It is designed to help developers to use PayPlug as +This is the documentation of Payplug's Python library. It is designed to help developers to use Payplug as payment solution in a simple, yet robust way. -You can create a PayPlug account at https://www.payplug.com. +You can create a Payplug account at https://www.payplug.com. Prerequisites ------------- -PayPlug's library relies on **python-requests>=1.0.1** to perform HTTP requests and requires **OpenSSL** to secure +Payplug's library relies on **python-requests>=1.0.1** to perform HTTP requests and requires **OpenSSL** to secure transactions. You also need either a **Python 2.6+** or a **Python 3.3+**. The library is known to work with these versions, pypy and pypy3. It may work on older versions or other Python implementations without warranty. If you use an implementation that is not listed above, do not hesitate to let us known if it worked for you or not, so that we can @@ -61,7 +61,7 @@ To get started, add the following piece of code to the header of your Python pro import payplug -If everything runs without errors, congratulations. You installed PayPlug python library! You're ready to create your +If everything runs without errors, congratulations. You installed Payplug python library! You're ready to create your first payment. Usage @@ -71,6 +71,8 @@ Here's how simple it is to create a payment request: .. sourcecode :: python + payplug.set_api_version("2019-08-06") + customer = { 'email': 'john.watson@example.net', 'first_name': 'John', @@ -80,6 +82,7 @@ Here's how simple it is to create a payment request: 'city': 'London', 'country': 'GB', } + payment_data = { 'amount': 3300, 'currency': 'EUR', @@ -94,6 +97,7 @@ Here's how simple it is to create a payment request: 'customer_id': 42710, }, } + payment = payplug.Payment.create(**payment_data) Go further: diff --git a/payplug/__init__.py b/payplug/__init__.py index d2b64b9..1ee40dd 100644 --- a/payplug/__init__.py +++ b/payplug/__init__.py @@ -416,3 +416,53 @@ def get_simulation(**data): http_client = HttpClient() response, _ = http_client.post(routes.url(routes.ONEY_PAYMENT_SIMULATION), data) return resources.OneyPaymentSimulation(**response) + + +class InstallmentPlan: + """ + A DAO for resources.InstallmentPlan which provides a way to query installment plans. + """ + + @staticmethod + def retrieve(installment_plan_id): + """ + Retrieve an installment plan from its id. + + :param installment_plan_id: The installment plan id + :type installment_plan_id: string + + :return: The installment plan resource + :rtype: resources.InstallmentPlan + """ + http_client = HttpClient() + response, __ = http_client.get(routes.url(routes.INSTALLMENT_PLANS, resource_id=installment_plan_id)) + return resources.InstallmentPlan(**response) + + + @staticmethod + def create(**data): + """ + Create an installment plan. + :param data: data required to create an installment plan + :return: The installment plan + :rtype resources.InstallmentPlan + """ + http_client = HttpClient() + response, _ = http_client.post(routes.url(routes.INSTALLMENT_PLANS), data) + return resources.InstallmentPlan(**response) + + + @staticmethod + def abort(installment_plan_id): + """ + Abort an installment plan. + :param installment_plan_id: The installment plan id + :type installment_plan_id: string + + :return: The installment plan + :rtype resources.InstallmentPlan + """ + abort = {"aborted": True} + http_client = HttpClient() + response, _ = http_client.patch(routes.url(routes.INSTALLMENT_PLANS, resource_id=installment_plan_id), abort) + return resources.InstallmentPlan(**response) diff --git a/payplug/__version__.py b/payplug/__version__.py index cf0bccd..b795e2c 100644 --- a/payplug/__version__.py +++ b/payplug/__version__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = '1.4.0' +__version__ = '1.4.1' diff --git a/payplug/resources.py b/payplug/resources.py index 81706a2..98842fc 100644 --- a/payplug/resources.py +++ b/payplug/resources.py @@ -391,3 +391,56 @@ class Operation(APIResource): An operation. """ pass + + +class InstallmentPlan(APIResource, VerifiableAPIResource, ReconstituableAPIResource): + """ + An InstallmentPlans Resource + """ + object_type = 'installment_plan' + + @property + def _mapper(self): + """ + Maps payment attributes to their specific types. + + :see :func:`~APIResource._mapper` + """ + return { + 'hosted_payment': Payment.HostedPayment, + 'notification': Payment.Notification, + 'failure': Payment.Failure, + 'billing': Payment.Billing, + 'shipping': Payment.Shipping, + } + + def _initialize(self, **resource_attributes): + """ + Initialize a resource. + Default behavior is just to set all the attributes. You may want to override this. + + :param resource_attributes: The resource attributes + """ + + schedules = [] + for schedule in resource_attributes.get('schedule', []): + schedules.append(InstallmentPlan.Schedule(**schedule)) + resource_attributes["schedule"] = schedules + super(InstallmentPlan, self)._initialize(**resource_attributes) + + def get_consistent_resource(self): + """ + :return an Installment Plan that you can trust. + :rtype InstallmentPlan + """ + http_client = HttpClient() + response, _ = http_client.get( + routes.url(routes.INSTALLMENT_PLANS, resource_id=self.id) + ) + return InstallmentPlan(**response) + + class Schedule(APIResource): + """ + Schedule information + """ + pass diff --git a/payplug/routes.py b/payplug/routes.py index 60fe7e1..562c27b 100644 --- a/payplug/routes.py +++ b/payplug/routes.py @@ -8,6 +8,7 @@ CARD_RESOURCE = CUSTOMER_RESOURCE + '/{customer_id}/cards' ACCOUNTING_REPORT_RESOURCE = '/accounting_reports' ONEY_PAYMENT_SIMULATION = '/oney_payment_simulations' +INSTALLMENT_PLANS = '/installment_plans' # API base url API_BASE_URL = 'https://api.payplug.com' diff --git a/payplug/test/test_init/test_dao_installment_plans.py b/payplug/test/test_init/test_dao_installment_plans.py new file mode 100644 index 0000000..c2038f0 --- /dev/null +++ b/payplug/test/test_init/test_dao_installment_plans.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from mock import patch +import payplug +from payplug import resources +from payplug.test import TestBase + +@patch('payplug.config.secret_key', 'a_secret_key') +@patch.object(payplug.HttpClient, 'get', lambda *args, **kwargs: ({'id': 'installment_plan_id'}, 200)) +@patch.object(payplug.HttpClient, 'patch', lambda *args, **kwargs: ({'id': 'installment_plan_id', 'failure': {'code': 'aborted'}}, 204)) +@patch.object(payplug.HttpClient, 'post', lambda *args, **kwargs: ({'id': 'installment_plan_id'}, 201)) +class TestInstallmentPlanRetrieve(TestBase): + def test_retrieve(self): + installment_plan = payplug.InstallmentPlan.retrieve('installment_plan_id') + + assert isinstance(installment_plan, resources.InstallmentPlan) + assert installment_plan.id == 'installment_plan_id' + + + def test_update(self): + installment_plan = payplug.InstallmentPlan.retrieve('installment_plan_id') + installment_plan = payplug.InstallmentPlan.abort(installment_plan.id) + + assert isinstance(installment_plan, resources.InstallmentPlan) + assert installment_plan.id == 'installment_plan_id' + assert installment_plan.failure.code == 'aborted' + + + def test_create(self): + installment_plan = payplug.InstallmentPlan.create() + + assert isinstance(installment_plan, resources.InstallmentPlan) + assert installment_plan.id == 'installment_plan_id' diff --git a/payplug/test/test_notifications.py b/payplug/test/test_notifications.py index 475e0c8..0227e3c 100644 --- a/payplug/test/test_notifications.py +++ b/payplug/test/test_notifications.py @@ -56,3 +56,111 @@ def test_treat_payment(self): def test_treat_binary_string(self): safe_payment = notifications.treat(b'{"id": "pay_test_unsafe", "object": "payment"}') assert safe_payment.id == 'pay_test' + + +@patch('payplug.config.secret_key', 'a_secret_key') +class TestTreatNotificationsInstallmentPlanSuccess(TestBase): + @classmethod + def setup_class(cls): + api_response = { + "hosted_payment": { + "cancel_url": "https://example.com/payment/payplug/cancel", + "return_url": "https://example.com/shop/payment/validate", + "payment_url": "https://secure.payplug.com/pay/test/59TXrYROSdmt0Y7W9Ubpg8" + }, + "customer": { + "phone_number": "None", + "city": "PARIS", + "first_name": "JOHN", + "last_name": "DOE", + "language": "fr", + "address1": "21 Elm Street", + "address2": "None", + "postcode": "75018", + "country": "France", + "email": "johndoe@example.com", + }, + "schedule": [ + {"date": "2021-08-04", "amount": 2600, "payment_ids": ["pay_59TXrYROSdmt0Y7W9Ubpg8"]}, + {"date": "2021-09-04", "amount": 2600, "payment_ids": []}, + {"date": "2021-10-04", "amount": 2600, "payment_ids": []}, + ], + "notification": {"url": "https://example.com/payment/payplug/ipn"}, + "created_at": 1628079236, + "object": "installment_plan", + "is_active": True, + "currency": "EUR", + "is_live": False, + "is_fully_paid": False, + "id": "inst_2aSCmLRZFtAA7arHJfGrE1", + "failure": "None", + "metadata": {"customer_id": "23", "acquirer_id": 5, "reference": "CMD0000123"}, + } + cls.patcher_get = patch.object(payplug.resources.HttpClient, 'get', return_value=(api_response, 200)) + cls.patcher_get.start() + + @classmethod + def teardown_class(cls): + cls.patcher_get.stop() + + def test_treat_installment_plan(self): + json_data = '''{ + "hosted_payment":{ + "cancel_url":"https://example.com/payment/payplug/cancel", + "return_url":"https://example.com/shop/payment/validate", + "payment_url":"https://secure.payplug.com/pay/test/59TXrYROSdmt0Y7W9Ubpg8" + }, + "customer":{ + "phone_number":"None", + "city":"PARIS", + "first_name":"JOHN", + "last_name":"DOE", + "language":"fr", + "address1":"21 Elm Street", + "address2":"None", + "postcode":"75018", + "country":"France", + "email":"johndoe@example.com" + }, + "schedule":[ + { + "date":"2021-08-04", + "amount":2600, + "payment_ids":[ + "pay_59TXrYROSdmt0Y7W9Ubpg8" + ] + }, + { + "date":"2021-09-04", + "amount":2600, + "payment_ids":[ + + ] + }, + { + "date":"2021-10-04", + "amount":2600, + "payment_ids":[ + + ] + } + ], + "notification":{ + "url":"https://example.com/payment/payplug/ipn" + }, + "created_at":1628079236, + "object":"installment_plan", + "is_active":true, + "currency":"EUR", + "is_live":false, + "is_fully_paid":false, + "id":"inst_2aSCmLRZFtAA7arHJfGrE1", + "failure":"None", + "metadata":{ + "customer_id":"23", + "acquirer_id":5, + "reference":"CMD0000123" + } + }''' + safe_installment_plan = notifications.treat(json_data) + assert safe_installment_plan.id == 'inst_2aSCmLRZFtAA7arHJfGrE1' diff --git a/payplug/test/test_resources/test_installment_plans.py b/payplug/test/test_resources/test_installment_plans.py new file mode 100644 index 0000000..e13b080 --- /dev/null +++ b/payplug/test/test_resources/test_installment_plans.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +from payplug.resources import InstallmentPlan, Payment +from payplug.test import TestBase + + +class TestInstallmentPlansResource(TestBase): + def test_initializer_installment_plans(self): + + null = None + true = True + false = False + + installment_plan_attributes = { + "id": "inst_1FTGSRWYHla7eDkfTo2Usd", + "object": "installment_plan", + "is_live": true, + "is_active": true, + "currency": "EUR", + "created_at": 1548326773, + "is_fully_paid": false, + "billing": { + "title": "mr", + "first_name": "John", + "last_name": "Watson", + "email": "john.watson@example.net", + "mobile_phone_number": null, + "landline_phone_number": null, + "address1": "221B Baker Street", + "address2": null, + "postcode": "NW16XE", + "city": "London", + "state": null, + "country": "GB", + "language": "en", + }, + "shipping": { + "title": "mr", + "first_name": "John", + "last_name": "Watson", + "email": "john.watson@example.net", + "mobile_phone_number": null, + "landline_phone_number": null, + "address1": "221B Baker Street", + "address2": null, + "postcode": "NW16XE", + "city": "London", + "state": null, + "country": "GB", + "language": "en", + "delivery_type": "BILLING", + }, + "hosted_payment": { + "payment_url": "https://secure.payplug.com/pay/1FTGSRWYHla7eDkfTo2Usd", + "return_url": "https://example.net/success?id=42", + "cancel_url": "https://example.net/cancel?id=42", + }, + "notification": {"url": "https://example.net/notifications?id=42"}, + "schedule": [ + { + "date": "2019-01-24", + "amount": 15000, + "payment_ids": ["pay_62VazeAbq5ttYietdC2QxR"], + }, + { + "date": "2019-02-24", + "amount": 15000, + "payment_ids": ["pay_12uazeAbq5ttYietdC2QxP"], + }, + {"date": "2019-03-24", "amount": 10000, "payment_ids": []}, + ], + "failure": { + "code": "a_failure_code", + "message": 'A weird failure message ®±' + }, + "metadata": {"customer_id": 42}, + } + + installment_plan_object = InstallmentPlan(**installment_plan_attributes) + + assert isinstance(installment_plan_object, InstallmentPlan) + assert installment_plan_object.created_at == 1548326773 + assert installment_plan_object.currency == "EUR" + assert installment_plan_object.is_fully_paid == False + + assert type(installment_plan_object.billing) == Payment.Billing + assert installment_plan_object.billing.title == 'mr' + assert installment_plan_object.billing.first_name == "John" + assert installment_plan_object.billing.last_name == "Watson" + assert installment_plan_object.billing.email == "john.watson@example.net" + assert installment_plan_object.billing.address1 == '221B Baker Street' + assert installment_plan_object.billing.postcode == 'NW16XE' + assert installment_plan_object.billing.city == 'London' + assert installment_plan_object.billing.country == 'GB' + assert installment_plan_object.billing.language == 'en' + + assert type(installment_plan_object.shipping) == Payment.Shipping + assert installment_plan_object.shipping.title == 'mr' + assert installment_plan_object.shipping.first_name == "John" + assert installment_plan_object.shipping.last_name == "Watson" + assert installment_plan_object.shipping.email == "john.watson@example.net" + assert installment_plan_object.shipping.address1 == '221B Baker Street' + assert installment_plan_object.shipping.postcode == 'NW16XE' + assert installment_plan_object.shipping.city == 'London' + assert installment_plan_object.shipping.country == 'GB' + assert installment_plan_object.shipping.language == 'en' + assert installment_plan_object.shipping.delivery_type == 'BILLING' + + assert type(installment_plan_object.hosted_payment) == Payment.HostedPayment + assert installment_plan_object.hosted_payment.payment_url == "https://secure.payplug.com/pay/1FTGSRWYHla7eDkfTo2Usd" + assert installment_plan_object.hosted_payment.return_url == "https://example.net/success?id=42" + assert installment_plan_object.hosted_payment.cancel_url == "https://example.net/cancel?id=42" + + + assert installment_plan_object.notification.url == "https://example.net/notifications?id=42" + assert type(installment_plan_object.schedule) == list + for schedule in installment_plan_object.schedule: + assert type(schedule) == InstallmentPlan.Schedule + assert type(schedule.date) == str + assert type(schedule.amount) == int + assert type(schedule.payment_ids) == list + for payment_id in schedule.payment_ids: + assert type(payment_id) == str \ No newline at end of file