diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3e5c53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# local settings +settings_local.py + +# IDE Pycharm +*.idea + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +# Executables +*.exe + +#Database +*.sqlite3 + +static diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a67574 --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bb3ec5f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b7d898 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +Iugu python API +============================================ + +This package is the more idiomatic python lib to work with Iugu service. The lib is compatible with +Iugu API v1 + +This package was original developed by horacioibrahim in https://github.com/horacioibrahim/iugu-python + +Updates +------- + +- Updated to Python 3 +- Create a new argument in create_charge, named **tokenid** if you want to work with Iugu.js + + +Overview +-------- + +This iugu-python lib is the more pythonic way to work with the webservices of payments iugu.com. This provides python objects to each entity of the service as Subscriptions, Plans, Customers, Invoices, etc. http://iugu.com/referencias/api - API Reference + +Prerequisites +------------- +In order to use the code in this package, you need to obtain an account +(API key) from http://iugu.com/referencias/api. You'll also find full API +documentation on that page. + +In order to run the sample code, you need a user account on the test mode +service where you will do your development. Sign up for an account at +https://iugu.com/signup and change mode in https://iugu.com/a/administration +("Modo de Teste") + +In order to run the client sample code, you need a account user token. This is +automatically created. See https://iugu.com/settings/profile + +Quick Install +----- +### Using pip ### +``` +pip install iugu-python +``` +or +### Using setup.py ### +``` +# Downloading package master or release: +# https://github.com/horacioibrahim/iugu-python/archive/master.zip +# or https://github.com/horacioibrahim/iugu-python/releases +unzip iugu-python-master.zip +cd iugu-python-master +python setup.py install +``` + +Usage (Quick Start) +----- +### Export environment variable IUGU_API_TOKEN ### +``` +# For linux users: +export IUGU_API_TOKEN=XXX +``` +### Merchant operations ### +``` +from iugu.merchant import IuguMerchant, Item +client = IuguMerchant(account_id="YOUR ACCOUN ID", + api_mode_test=True) +token = client.create_payment_token('4111111111111111', 'JA', 'Silva', + '12', '2010', '123') + => https://api.iugu.com/v1/payment_token +``` +How create an item for charge +``` +item = Item("Produto My Test", 1, 10000) +``` +Now you can to create charge +```python +charge = client.create_charge(EMAIL_CUSTOMER, item, token=token.id) +``` +Check invoice ID created. All payment pass by Invoice. +``` +charge.invoice_id +``` +Or create a blank_slip ("Boleto Bancário") +``` +charge = client.create_charge(EMAIL_CUSTOMER, item) + => https://api.iugu.com/v1/charge +``` +### Customer operations ### +```python +from iugu.customers import IuguCustomer +client = IuguCustomer() +customer = client.create(email='your_customer@example.com') + => https://api.iugu.com/v1/customers +``` +Now you can to retrieve customer +``` +client.get(customer_id) +``` +You can edit existent customer +``` +client.set(CUSTOMER_ID, name="Sub Zero Wins") +``` +Or you can to use save() +``` +customer.name = "Sub Zero Wins" +customer.save() +``` +To remove or delete customer +``` +client.delete(CONSUMER_ID) # by id + or +customer.remove() # by current instance +``` +### Operations with lists of customer ### +Get all customer +```python +from iugu.customers import IuguCustomer +client = IuguCustomer() +# your flavor of options +# client.getitems([limit, skip, query, sort, created_at_from, created_at_to, +# updated_since]) + +Use one option per time. Samples: +client.getitems(limit=30) # Get most recent (latest) 30 customers +client.getitems(skip=14) # Skip X customers. Useful for pagination +client.getitems(updated_since="2014-06-05T15:02:40-03:00") + +In tests SORT is not support by API: +client.getitems(sort="-name") # Sort by field >>name<< (descending) +client.getitems(sort="name") # Sort by field >>name<< (ascending) + + => http://iugu.com/referencias/api#listar-os-clientes +``` +### Operations with Invoices ### +Create an invoice +``` +from iugu.invoices import IuguInvoice +from iugu.merchant import Item + +item = Item("Curso: High Self Learning", 1, 6900) # qtd:1; price: 69,00 +invoice_obj = IuguInvoice() +new_invoice = invoice_obj.create(due_date='24/06/2014', + email='customer@example.com', items=item) +``` +Get invoice by id +``` +# not is need previous instance/obj (classmethod) +invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') +``` +Get all invoices +``` +# not is need previous instance/obj +invoices = IuguInvoice.getitems() # outcomes list of invoices (max 100 by API) +``` +Get all with filter +``` +invoices = IuguInvoice.getitems(limit=10) +invoices = IuguInvoice.getitems(skp=5) +invoices = IuguInvoice.getitems(sort="-email") # DESC +invoices = IuguInvoice.getitems(sort="email") # ASC +... +``` +Edit/change invoice. Only invoices with status "draft" can be changed all fields +otherwise (if status pending, cancel or paid) only the logs field can to change +``` +invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') +invoice_existent.email = "other@other.com" +invoice_existent.save() +``` +Remove +``` +invoice_existent.remove() +``` +Cancel +``` +IuguInvoice.to_cancel('A4AF853BC5714380A8708B2A4EDA27B3') +``` +Refund +``` +invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') +invoice_existent.refund() +``` +### Operations with Subscriptions ### +Create a subscription +``` +from subscriptions import IuguSubscription +client = IuguSubscription() +# from plans import IuguPan +# plan_x = IuguPlan().create("Plano Collor", "plano_collor") +# from customers import IuguCustomer +# mario = IuguCustomer().create(email='supermario@gmail.com') +# subscription = client.create(customer_id=mario.id, +# plan_identifier=plan_x.identifier) +subscription = client.create(customer_id="XXX", plan_identifier="XXX") +``` +Get one +``` +subscription = IuguSubscription.get('ID') +``` +Edit/Change +``` +subscription = IuguSubscription.get('ID') +subscription.expires_at = "14/07/2014" +subscription.save() +``` +Remove +``` +subscription.remove() +``` + +### References ### +- API Document: http://iugu.com/referencias/api + + +Known Issues +------------ +### Date Types ### +It's need to use date formatted as string "2014-06-05T15:02:40-03:00", +but in new release date will python date. diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/iugu/__init__.py b/lib/iugu/__init__.py new file mode 100644 index 0000000..fa95286 --- /dev/null +++ b/lib/iugu/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2009-2014 hipy.co, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Iugu client for Iugu API.""" diff --git a/lib/iugu/base.py b/lib/iugu/base.py new file mode 100644 index 0000000..cda342c --- /dev/null +++ b/lib/iugu/base.py @@ -0,0 +1,166 @@ +# coding: utf-8 +__author__ = 'horacioibrahim' + +import os +from http.client import HTTPSConnection, CannotSendRequest, BadStatusLine +from urllib.parse import urlencode +from json import load as json_load + +# python-iugu package modules +from . import errors, config + +class IuguApi(object): + + """ + Contains basics info to requests with API + + account_id: + api_user: + api_mode_test: + """ + try: + API_TOKEN = os.environ["IUGU_API_TOKEN"] #config.API_TOKEN + except KeyError: + raise errors.IuguConfigException("Required environment variable " \ + "IUGU_API_TOKEN") + + def __init__(self, **options): + self.account_id = options.get('account_id') + self.api_user = options.get('api_user') + self.api_mode_test = options.get('api_mode_test') # useful for payment_token + + def is_debug(self): + """Returns debug mode in config""" + return config.DEBUG + + def is_mode_test(self): + """Check if api_mode_test is True or False. + Return string of the boolean + """ + + mode = "true" if self.api_mode_test is True else "false" + return mode + + def custom_variables_list(self, custom_variables): + """Unpacking dictionary of keywords arguments and returns a list with + data fit for to send API""" + + custom_data = [] # used to extend custom_variables in data_set() + if isinstance(custom_variables, dict): + # TODO: list comprehensions + for k, v in list(custom_variables.items()): + custom_data.append(("custom_variables[][name]", k.lower())) + custom_data.append(("custom_variables[][value]", v)) + + if custom_data: + return custom_data + + return None + + +class IuguRequests(IuguApi): + + """ + All request to API pass by here. Use the HTTP verbs for each request. For + each method (get, post, put and delete) is need an URN and a list of fields + its passed as list of tuples that is encoded by urlencode (e.g: + [("field_api", "value")] + + URN: is relative path of URL http://api.iugu.com/ARG1/ARG2 where + URN = "/ARG1/ARG2" + + All methods appends an api_token that is encoded in url params. The + api_token is given in config.py its useful to work in sandbox mode. + + :method get: make a GET request + :method post: make a POST request + :method put: make a PUT request + :method delete: make a DELETE request + """ + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + __conn = HTTPSConnection(config.API_HOSTNAME) # not put in instance + __conn.timeout = 10 + + def __init__(self, **options): + super(IuguRequests, self).__init__(**options) + + if self.is_debug(): + # set debuglevel to HTTPSConnection + self.__conn.set_debuglevel(2) + + def __validation(self, response, msg=None): + """ + Validates if data returned by API contains errors json. The API returns + by default a json with errors as field {errors: XXX} + + => http://iugu.com/referencias/api#erros + """ + import codecs + + reader = codecs.getreader("utf-8") + + results = json_load(reader(response)) + + try: + err = results['errors'] + except: + err = None + + if err: + raise errors.IuguGeneralException(value=err) + else: + return results + + def __reload_conn(self): + """ + Wrapper to keep TCP connection ESTABLISHED. Rather the connection go to + CLOSE_WAIT and raise errors CannotSendRequest or the server reply with + empty and it raise BadStatusLine + """ + self.__conn = HTTPSConnection(config.API_HOSTNAME) # reload + self.__conn.timeout = 10 + + def __conn_request(self, http_verb, urn, params): + """ + Wrapper to request/response of httplib's context, reload a + connection if presume that error will occurs and returns the response + """ + try: + self.__conn.request(http_verb, urn, params, self.headers) + except CannotSendRequest: + self.__reload_conn() + self.__conn.request(http_verb, urn, params, self.headers) + + try: + response = self.__conn.getresponse() + except (IOError, BadStatusLine): + self.__reload_conn() + self.__conn.request(http_verb, urn, params, self.headers) + response = self.__conn.getresponse() + + return response + + def get(self, urn, fields): + fields.append(("api_token", self.API_TOKEN)) + params = urlencode(fields, True) + response = self.__conn_request("GET", urn, params) + return self.__validation(response) + + def post(self, urn, fields): + fields.append(("api_token", self.API_TOKEN)) + params = urlencode(fields, True) + response = self.__conn_request("POST", urn, params) + return self.__validation(response) + + def put(self, urn, fields): + fields.append(("api_token", self.API_TOKEN)) + params = urlencode(fields, True) + response = self.__conn_request("PUT", urn, params) + return self.__validation(response) + + def delete(self, urn, fields): + fields.append(("api_token", self.API_TOKEN)) + params = urlencode(fields, True) + response = self.__conn_request("DELETE", urn, params) + return self.__validation(response) diff --git a/lib/iugu/config.py b/lib/iugu/config.py new file mode 100644 index 0000000..9dca801 --- /dev/null +++ b/lib/iugu/config.py @@ -0,0 +1,10 @@ +__author__ = 'horacioibrahim' + + +METHOD_PAYMENT_CREDIT_CARD = "credit_card" + +DEBUG = False # Print logs/messages in console. + + +# No share data +API_HOSTNAME = "api.iugu.com" diff --git a/lib/iugu/customers.py b/lib/iugu/customers.py new file mode 100644 index 0000000..357d8d4 --- /dev/null +++ b/lib/iugu/customers.py @@ -0,0 +1,420 @@ +# -*- coding: utf-8 -*- + +__author__ = 'horacioibrahim' + +# python-iugu package modules +from . import base, config, errors + +class IuguCustomer(base.IuguApi): + + __conn = base.IuguRequests() + + def __init__(self, **options): + """ + This class is a CRUD for customers in API + + :param **options: receives dictionary load by JSON with fields of API + + => http://iugu.com/referencias/api#clientes + """ + super(IuguCustomer, self).__init__(**options) + self.id = options.get("id") + self.email = options.get("email") + self.default_payment_method_id = options.get("default_payment_method_id") + self.name = options.get("name") + self.notes = options.get("notes") + # TODO: convert str date in date type + # year, month, day = map(int, string_date.split('-')) + # date_converted = Date(day, month, year) + self.created_at = options.get("created_at") + # TODO: convert str date in date type + self.updated_at = options.get("updated_at") + self.custom_variables = options.get("custom_variables") + self.payment = IuguPaymentMethod(self) + + def create(self, name=None, notes=None, email=None, custom_variables=None): + """Creates a customer and return an IuguCustomer's instance + + :param name: customer name + :param notes: field to post info's of an user + :param email: required data of an user + :param custom_variables: a dict {'key':'value'} + """ + data = [] + urn = "/v1/customers" + + if name: + data.append(("name", name)) + + if notes: + data.append(("notes", notes)) + + if email: + self.email = email + + if self.email: + data.append(("email", self.email)) + else: + raise errors.IuguGeneralException(value="E-mail required is empty") + + if custom_variables: + custom_data = self.custom_variables_list(custom_variables) + data.extend(custom_data) + customer = self.__conn.post(urn, data) + instance = IuguCustomer(**customer) + + return instance + + def set(self, customer_id, name=None, notes=None, custom_variables=None): + """ Updates/changes a customer that already exists + + :param custom_variables: is a dict {'key':'value'} + HINT: Use method save() at handling an instance + """ + data = [] + urn = "/v1/customers/{customer_id}".format(customer_id=str(customer_id)) + + if name: + data.append(("name", name)) + + if notes: + data.append(("notes", notes)) + + if custom_variables: + custom_data = self.custom_variables_list(custom_variables) + data.extend(custom_data) + + customer = self.__conn.put(urn, data) + + return IuguCustomer(**customer) + + def save(self): + """Save updating a customer's instance""" + return self.set(self.id, name=self.name, notes=self.notes) + + @classmethod + def get(self, customer_id): + """Gets one customer based in iD and returns an instance""" + data = [] + urn = "/v1/customers/{customer_id}".format(customer_id=str(customer_id)) + customer = self.__conn.get(urn, data) + instance = IuguCustomer(**customer) + + return instance + + @classmethod + def getitems(self, limit=None, skip=None, created_at_from=None, + created_at_to=None, query=None, updated_since=None, sort=None): + """ + Get a list of customers and return a list of IuguCustomer's instances. + + :param limit: limits the number of customers returned by API (default + and immutable of API is 100) + :param skip: skips a numbers of customers where more recent insert + ordering. Useful to pagination + :param query: filters based in value (case insensitive) + :param sort: sorts based in field. Use minus signal to determine the + direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API + :return: list of IuguCustomer instances + """ + data = [] + urn = "/v1/customers/" + + # Set options + if limit: + data.append(("limit", limit)) + if skip: + data.append(("start", skip)) + if created_at_from: + data.append(("created_at_from", created_at_from)) + if created_at_to: + data.append(("created_at_to", created_at_to)) + if updated_since: + data.append(("updated_since", updated_since)) + if query: + data.append(("query", query)) + + # TODO: sort not work fine. Waiting support of API providers + if sort: + assert sort is not str, "sort must be string as -name or name" + + if sort.startswith("-"): + sort = sort[1:] + key = "sortBy[{field}]".format(field=sort) + data.append((key, "desc")) + else: + key = "sortBy[{field}]".format(field=sort) + data.append((key, "asc")) + + customers = self.__conn.get(urn, data) + + #TODO: list comprehensions + customers_objects = [] + for customer in customers["items"]: + obj_customer = IuguCustomer(**customer) + customers_objects.append(obj_customer) + + return customers_objects + + def delete(self, customer_id=None): + """Deletes a customer of instance or by id. + And return the removed object""" + data = [] + + if self.id: + # instance of class (customer already exist) + _customer_id = self.id + else: + if customer_id: + _customer_id = customer_id + else: + # instance of class (not saved) + raise TypeError("It's not instance of object returned or " \ + "customer_id is empty.") + + urn = "/v1/customers/" + str(_customer_id) + customer = self.__conn.delete(urn, data) + + return IuguCustomer(**customer) + + remove = delete # remove for keep the semantic of API + + +class IuguPaymentMethod(object): + + """ + A customer have multiple payments methods with only one default. This class + allows handling Payment Methods. + + => http://iugu.com/referencias/api#formas-de-pagamento-de-cliente + """ + + def __init__(self, customer, item_type="credit_card", **kwargs): + assert isinstance(customer, IuguCustomer), "Customer invalid." + _data = kwargs.get('data') + self.customer_id = kwargs.get('customer_id') # useful create Payment by customer ID + self.customer = customer + self.description = kwargs.get('description') + self.item_type = item_type # support only credit_card + if _data: + self.token = _data.get('token') # data credit card token + self.display_number = _data.get('display_number') + self.brand = _data.get('brand') + self.holder_name = _data.get('holder_name') + # self.set_as_default = kwargs.get('set_as_default') + self.id = kwargs.get('id') + + # constructor payment + data = kwargs.get('data') + if data and isinstance(data, dict): + self.payment_data = PaymentTypeCreditCard(**data) + else: + self.payment_data = PaymentTypeCreditCard() + + self.__conn = base.IuguRequests() + + def create(self, customer_id=None, description=None, number=None, + verification_value=None, first_name=None, last_name=None, + month=None, year=None, token=None, set_as_default=False): + """ Creates a payment method for a customer and returns the own class + + :param customer_id: id of customer. You can pass in init or here + :param description: required to create method. You can pass in init + or here + + IMPORTANT: The API assert that data is optional, but is not real + behavior. The values as number, verification_value, first_name, last_name, + month and year are required args. + """ + data = [] + + # check if customer_id + if customer_id: + self.customer_id = customer_id + else: + self.customer_id = self.customer.id + + if description: + self.description = description + + # we can create description when to instance or here (in create) + assert self.description is not None, "description is required" + + if self.customer_id: + urn = "/v1/customers/{customer_id}/payment_methods" \ + .format(customer_id=str(self.customer.id)) + else: + raise errors.IuguPaymentMethodException + + # mounting data... + data.append(("description", self.description)) + data.append(("set_as_default", set_as_default)) + + if token: + # if has token, card data it isn't need. + self.token = token + data.append(("token", self.token )) + else: + data.append(("item_type", self.item_type )) + if number: + self.payment_data.number = number + + if verification_value: + self.payment_data.verification_value = verification_value + + if first_name: + self.payment_data.first_name = first_name + + if last_name: + self.payment_data.last_name = last_name + + if month: + self.payment_data.month = month + + if year: + self.payment_data.year = year + + if self.payment_data.is_valid(): + # It's possible create payment method without credit card data. + # Therefore this check is need. + payment = self.payment_data.to_data() + data.extend(payment) + + response = self.__conn.post(urn, data) + + return IuguPaymentMethod(self.customer, **response) + + def get(self, payment_id, customer_id=None): + """ Returns a payment method of an user with base payment ID""" + data = [] + payment_id = str(payment_id) + + if customer_id is None: + if self.customer.id: + customer_id = self.customer.id + else: + raise TypeError("Customer or customer_id is not be None") + + urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ + format(customer_id=customer_id, payment_id=payment_id) + response = self.__conn.get(urn, data) + + return IuguPaymentMethod(self.customer, **response) + + def getitems(self, customer_id=None): + """ + Gets payment methods of a customer and returns a list of payment's + methods instances (API limit is 100) + """ + data = [] + + if customer_id is None: + customer_id = self.customer.id + + urn = "/v1/customers/{customer_id}/payment_methods".\ + format(customer_id=customer_id) + + response = self.__conn.get(urn, data) + payments = [] + + for payment in response: + obj_payment = IuguPaymentMethod(self.customer, **payment) + payments.append(obj_payment) + + return payments + + def set(self, payment_id, description, customer_id=None, + set_as_default=False): + """Updates/changes payment method with based in payment ID and customer. + And returns object edited. + + HINT: Use save() to modify instances + """ + data = [] + data.append(("description", description)) + data.append(("set_as_default", set_as_default)) + + if customer_id is None: + customer_id = self.customer.id + + urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ + format(customer_id=customer_id, payment_id=payment_id) + response = self.__conn.put(urn, data) + + return IuguPaymentMethod(self.customer, **response) + + def save(self): + return self.set(self.id, self.description) + + + def delete(self, payment_id, customer_id=None): + """Deletes payment method with based in ID and customer. And + returns object edited. + + HINT: Use remove() for to remove instances + """ + data = [] + + if customer_id is None: + customer_id = self.customer.id + + urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ + format(customer_id=customer_id, payment_id=payment_id) + + response = self.__conn.delete(urn, data) + + return IuguPaymentMethod(self.customer, **response) + + def remove(self): + assert self.id is not None, "Invalid: IuguPaymentMethod not have ID." + return self.delete(self.id) + + +class PaymentTypeCreditCard(object): + + """ + + This class abstract the data parameter of payment method context + + :method is_valid: check if required fields is correct + :method to_data: returns the data in format to be encoded by urllib as a + list of tuples + + """ + + def __init__(self, **kwargs): + self.number = kwargs.get('number') + self.verification_value = kwargs.get('verification_value') + self.first_name = kwargs.get('first_name') + self.last_name = kwargs.get('last_name') + self.month = kwargs.get('month') + self.year = kwargs.get('year') + self.display_number = kwargs.get('display_number') + self.token = kwargs.get('token') + self.brand = kwargs.get('brand') + + def is_valid(self): + """Required to send to API""" + if self.number and self.verification_value and self.first_name and \ + self.last_name and self.month and self.year: + return True + else: + return False + + def to_data(self): + """ + Returns a list of tuples with ("data[field]", value). Use it to + return a data that will extend the data params in request. + """ + # control to required fields + if not self.is_valid(): + blanks = [ k for k, v in list(self.__dict__.items()) if v is None] + raise TypeError("All fields required to %s. Blank fields given %s" % + (self.__class__, blanks)) + + data = [] + for k, v in list(self.__dict__.items()): + key = "data[{key_name}]".format(key_name=k) + data.append((key, v)) + + return data diff --git a/lib/iugu/errors.py b/lib/iugu/errors.py new file mode 100644 index 0000000..bea6fe5 --- /dev/null +++ b/lib/iugu/errors.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +class IuguConfigException(BaseException): + + def __init__(self, value="Required environment variable IUGU_API_TOKEN"): + self.value = value + + def __str__(self): + return repr(self.value) + +class IuguConfigTestsErrors(BaseException): + + def __init__(self, value="Required environment variables"): + self.value = value + + def __str__(self): + return repr(self.value) + +class IuguPaymentMethodException(BaseException): + + def __init__(self, value="Required customer_id must not be blank or None"): + self.value = value + + def __str__(self): + return repr(self.value) + + +class IuguGeneralException(BaseException): + + def __init__(self, value="Data returned not matches with iugu-python classes"): + self.value = value + + def __str__(self): + return repr(self.value) + + +class IuguInvoiceException(BaseException): + + def __init__(self, value="Incomplete request for create invoices"): + self.value = value + + def __str__(self): + return repr(self.value) + + +class IuguPlansException(BaseException): + + def __init__(self, value="Incomplete request for create plans"): + self.value = value + + def __str__(self): + return repr(self.value) + +class IuguSubscriptionsException(BaseException): + + def __init__(self, value="Invalid request for Subscriptions"): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/lib/iugu/invoices.py b/lib/iugu/invoices.py new file mode 100644 index 0000000..831034f --- /dev/null +++ b/lib/iugu/invoices.py @@ -0,0 +1,402 @@ +__author__ = 'horacioibrahim' + + +# python-iugu package modules +from . import merchant, config, base, errors + +class IuguInvoice(base.IuguApi): + + """ + + This class allows handling invoices. The invoice is used to customers to + make payments. + + :attribute class data: is a descriptor that carries rules of API fields. + Only fields not None or not Blank can be sent. + :attribute status: Accept two option: draft and pending, but can be + draft, pending, [paid and canceled (internal use)] + :attribute logs: is instanced a dictionary like JSON + :attribute bank_slip: is instanced a dictionary like JSON + + => http://iugu.com/referencias/api#faturas + """ + + __conn = base.IuguRequests() + + def __init__(self, item=None, **kwargs): + super(IuguInvoice, self).__init__(**kwargs) + self.id = kwargs.get("id") + self.due_date = kwargs.get("due_date") + self.currency = kwargs.get("currency") + self.discount_cents = kwargs.get("discount_cents") + # self.customer_email = kwargs.get("customer_email") + self.email = kwargs.get("email") # customer email + self.items_total_cents = kwargs.get("items_total_cents") + self.notification_url = kwargs.get("notification_url") + self.return_url = kwargs.get("return_url") + self.status = kwargs.get("status") # [draft,pending] internal:[paid,canceled] + self.expiration_url = kwargs.get("expiration_url") + self.tax_cents = kwargs.get("tax_cents") + self.updated_at = kwargs.get("updated_at") + self.total_cents = kwargs.get("total_cents") + self.paid_at = kwargs.get("paid_at") + self.secure_id = kwargs.get("secure_id") + self.secure_url = kwargs.get("secure_url") + self.customer_id = kwargs.get("customer_id") + self.user_id = kwargs.get("user_id") + self.total = kwargs.get("total") + self.created_at = kwargs.get("created_at") + self.taxes_paid = kwargs.get("taxes_paid") + self.interest = kwargs.get("interest") + self.discount = kwargs.get("discount") + self.refundable = kwargs.get("refundable") + self.installments = kwargs.get("installments") + self.bank_slip = kwargs.get("bank_slip") # TODO: create a class/object. + self.logs = kwargs.get("logs") # TODO: create a class/object + # TODO: descriptors (getter/setter) for items + _items = kwargs.get("items") + self.items = None + + if _items: + # TODO: list comprehensions + _list_items = [] + for i in _items: + obj_item = merchant.Item(**i) + _list_items.append(obj_item) + self.items = _list_items + else: + if item: + assert isinstance(item, merchant.Item), "item must be instance of Item" + self.items = item + + self.variables = kwargs.get("variables") + self.logs = kwargs.get("logs") + self.custom_variables = kwargs.get("custom_variables") + self._data = None + + # constructor of data descriptors + def data_get(self): + return self._data + + def data_set(self, kwargs): + draft = kwargs.get("draft") + return_url = kwargs.get("return_url") + expired_url = kwargs.get("expired_url") + notification_url = kwargs.get("notification_url") + tax_cents = kwargs.get("tax_cents") + discount_cents = kwargs.get("discount_cents") + customer_id = kwargs.get("customer_id") + ignore_due_email = kwargs.get("ignore_due_email") + subscription_id = kwargs.get("subscription_id") + due_date = kwargs.get("due_date") + credits = kwargs.get("credits") + items = kwargs.get("items") + email = kwargs.get("email") + custom_data = kwargs.get("custom_data") + + data = [] + + if draft: + data.append(("status", "draft")) # default is pending + + # data will posted and can't null, None or blank + if return_url: + self.return_url = return_url + + if self.return_url: + data.append(("return_url", self.return_url)) + + if expired_url: + self.expiration_url = expired_url + + if self.expiration_url: + data.append(("expired_url", self.expiration_url)) + + if notification_url: + self.notification_url = notification_url + + if self.notification_url: + data.append(("notification_url", self.notification_url)) + + if tax_cents: + self.tax_cents = tax_cents + + data.append(("tax_cents", self.tax_cents)) + + if discount_cents: + self.discount_cents = discount_cents + + data.append(("discount_cents", self.discount_cents)) + + if customer_id: + self.customer_id = customer_id + + if self.customer_id: + data.append(("customer_id", self.customer_id)) + + if credits: + data.append(("credits", credits)) + + if ignore_due_email: + data.append(("ignore_due_email", True)) + + if subscription_id: + data.append(("subscription_id", subscription_id)) + + if due_date: + self.due_date = due_date + + if self.due_date: + data.append(("due_date", self.due_date)) + + if isinstance(items, list): + for item in items: + data.extend(item.to_data()) + else: + if items is not None: + data.extend(items.to_data()) + + if email: + self.email = email + + if self.email: + data.append(("email", self.email)) + + if custom_data: + data.extend(custom_data) + + self._data = data + + def data_del(self): + del self._data + + data = property(data_get, data_set, data_del, "data property set/get/del") + + def create(self, draft=False, return_url=None, email=None, expired_url=None, + notification_url=None, tax_cents=None, discount_cents=None, + customer_id=None, ignore_due_email=False, subscription_id=None, + credits=None, due_date=None, items=None, custom_variables=None): + """ + Creates an invoice and returns owns class + + :param subscription_id: must be existent subscription from API + :param customer_id: must be API customer_id (existent customer) + :param items: must be item instance of merchant.Item() + :para custom_variables: a dict {'key':'value'} + + => http://iugu.com/referencias/api#faturas + """ + urn = "/v1/invoices" + + # handling required fields + if not due_date: + if self.due_date: + # due_date is required. If it not passed in args, it must to + # exist at least in instance object + due_date = self.due_date # "force" declaring locally + else: + raise errors.IuguInvoiceException(value="Required due_date is" \ + " empty.") + + if not items: + if self.items: + # items are required. If it not passed as args, + # it must to exist at least in instance object + items = self.items # "force" declaring locally + else: + raise errors.IuguInvoiceException(value="Required items is" \ + " empty.") + + if not email: + if self.email: + # email is required. If it not passed as args, + # it must to exist at least in instance object + email = self.email # "force" declaring locally + else: + raise errors.IuguInvoiceException(value="Required customer" \ + " email is empty.") + + if custom_variables: + custom_data = self.custom_variables_list(custom_variables) + # to declare all variables local before calling locals().copy() + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self.__conn.post(urn, self.data) + invoice = IuguInvoice(**response) + return invoice + + def set(self, invoice_id, email=None, due_date=None, + return_url=None, expired_url=None, notification_url=None, + tax_cents=None, discount_cents=None, customer_id=None, + ignore_due_email=False, subscription_id=None, credits=None, + items=None, custom_variables=None): + """ Updates/changes a invoice that already exists + + :param custom_variables: a dict {'key', value}. If previously values + exist the variable is edited rather is added + + IMPORTANT: Only invoices with status "draft" can be changed all fields + otherwise (if status pending, cancel or paid) only the field logs + can to change. + """ + urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) + + if items is not None: + assert isinstance(items, merchant.Item), "item must be instance of Item" + + if custom_variables: + custom_data = self.custom_variables_list(custom_variables) + # to declare all variables local before calling locals().copy() + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self.__conn.put(urn, self.data) + + return IuguInvoice(**response) + + def save(self): + """Save updating a invoice's instance. To add/change custom_variables + keywords to use create() or set() + + IMPORTANT: Only invoices with status "draft" can be changed + """ + self.data = self.__dict__ + urn = "/v1/invoices/{invoice_id}".format(invoice_id=self.id) + response = self.__conn.put(urn, self.data) + + return IuguInvoice(**response) + + @classmethod + def get(self, invoice_id): + """Gets one invoice with base in invoice_id and returns instance""" + data = [] + urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) + response = self.__conn.get(urn, data) + + return IuguInvoice(**response) + + @classmethod + def getitems(self, limit=None, skip=None, created_at_from=None, + created_at_to=None, query=None, updated_since=None, sort=None, + customer_id=None): + """ + Gets a list of invoices where the API default is limited 100. Returns + a list of IuguInvoice + + :param limit: limits the number of invoices returned by API + :param skip: skips a numbers of invoices where more recent insert + ordering. Useful to pagination. + :param query: filters based in value (case insensitive) + :param sort: sorts based in field. Use minus signal to determine the + direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API + :return: list of IuguInvoice instances + """ + data = [] + urn = "/v1/invoices/" + + # Set options + if limit: + data.append(("limit", limit)) + if skip: + data.append(("start", skip)) + if created_at_from: + data.append(("created_at_from", created_at_from)) + if created_at_to: + data.append(("created_at_to", created_at_to)) + if updated_since: + data.append(("updated_since", updated_since)) + if query: + data.append(("query", query)) + if customer_id: + data.append(("customer_id", customer_id)) + + # TODO: sort not work fine. Waiting support of API providers + if sort: + assert sort is not str, "sort must be string as -name or name" + + if sort.startswith("-"): + sort = sort[1:] + key = "sortBy[{field}]".format(field=sort) + data.append((key, "desc")) + else: + key = "sortBy[{field}]".format(field=sort) + data.append((key, "asc")) + + invoices = self.__conn.get(urn, data) + #TODO: list comprehensions + invoices_objects = [] + for invoice_item in invoices["items"]: + obj_invoice = IuguInvoice(**invoice_item) + invoices_objects.append(obj_invoice) + + return invoices_objects + + def remove(self, invoice_id=None): + """ + Removes an invoice by id or instance and returns None + """ + invoice_id = invoice_id if invoice_id else self.id + if invoice_id is None: + raise errors.IuguSubscriptionsException(value="ID (invoice_id) can't be empty") + + urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) + response = self.__conn.delete(urn, []) + obj = IuguInvoice(**response) + # TODO: list comprehensions ? + if obj: + for k, v in list(self.__dict__.items()): + self.__dict__[k] = None + + def cancel(self): + """Cancels an instance of invoice and returns own invoice with status + canceled""" + urn = "/v1/invoices/{invoice_id}/cancel".format(invoice_id=self.id) + + # This below if to avoid a request because the API not allow this operation + # but all API can to change theirs behaviors so to allow to cancel + # invoices with status difference of "pending". + # The approach without if also to raise exception with error from directly + # API responses but here the focus is less requests. + if self.status == "pending": + response = self.__conn.put(urn, []) + obj = IuguInvoice(**response) + else: + raise errors.IuguGeneralException(value="Cancel operation support only " \ + "invoices with status: pending.") + + return obj + + @classmethod + def to_cancel(self, invoice_id): + """Cancels an invoice with base in invoice ID and returns own + invoice with status canceled + + => http://iugu.com/referencias/api#cancelar-uma-fatura + """ + urn = "/v1/invoices/{invoice_id}/cancel".format(invoice_id=invoice_id) + response = self.__conn.put(urn, []) + obj = IuguInvoice(**response) + + return obj + + def refund(self): + """Makes refund of an instance of invoice + + => http://iugu.com/referencias/api#reembolsar-uma-fatura + """ + urn = "/v1/invoices/{invoice_id}/refund".format(invoice_id=self.id) + + # This below if to avoid a request because the API not allow this operation + # but all API can to change theirs behaviors so to allow to refund + # invoices with status difference of "paid". + # The approach without if also to raise exception with error from directly + # API responses but here the focus is less requests. + if self.status == "paid": + response = self.__conn.post(urn, []) + obj = IuguInvoice(**response) + else: + raise errors.IuguGeneralException(value="Refund operation support only " \ + "invoices with status: paid.") + + return obj diff --git a/lib/iugu/merchant.py b/lib/iugu/merchant.py new file mode 100644 index 0000000..dc6541a --- /dev/null +++ b/lib/iugu/merchant.py @@ -0,0 +1,282 @@ +# coding: utf-8 + +import http.client +import json +from urllib.parse import urlencode +from urllib.request import urlopen, Request + +# python-iugu package modules +from . import base, config + + +class IuguMerchant(base.IuguApi): + + def __init__(self, **kwargs): + super(IuguMerchant, self).__init__(**kwargs) + self.__conn = base.IuguRequests() + + def create_payment_token(self, card_number, first_name, last_name, + month, year, verification_value, method="credit_card"): + """Sends credit_card data of a customer and returns a token + for payment process without needing to persist personal data + of customers. + + :param method: string 'credit_card' or options given by API. + :param card_number: str of card number + :param first_name: string with consumer/buyer first name + :param last_name: consumer/buyer last name + :param month: two digits to Month expiry date of card + :param year: four digits to Year expiry date of card + :param verification_value: CVV + :returns: token_id as id, response, extra_info and method + + => http://iugu.com/referencias/api#tokens-e-cobranca-direta + """ + urn = "/v1/payment_token" + data = [('data[last_name]', last_name), ('data[first_name]', first_name), + ('data[verification_value]', verification_value), + ('data[month]', month), ('data[year]', year), + ('data[number]', card_number)] + + data.append(("account_id", self.account_id)) # work less this + data.append(("test", self.is_mode_test())) + data.append(("method", method)) + token_data = self.__conn.post(urn, data) + + return Token(token_data) + + def create_charge(self, consumer_email, items, token=None, tokenid=None, payer=None): + """ + Creates an invoice and returns a direct charge done. + + :param items: is instance of class of the merchant.Item + :param token: an instance of Token. It's used to credit card payments. + If argument token is None it's used to method=bank_slip + """ + # TODO: payer and address support + data = [] # data fields of charge. It'll encode + urn = "/v1/charge" + + if isinstance(items, list): + for item in items: + assert type(item) is Item + data.extend(item.to_data()) + else: + assert type(items) is Item + data.extend(items.to_data()) + + if tokenid: + data.append(("token", tokenid)) + else: + if token and isinstance(token, Token): + token_id = token.id + data.append(("token", token_id)) + else: + data.append(("method", "bank_slip")) + + data.append(("email", consumer_email)) + results = self.__conn.post(urn, data) + + return Charge(results) + + +class Charge(object): + + """ + This class receives response of request create_charge. Useful only to view + status and invoice_id + + :attribute invoice_id: ID of Invoice created + + """ + + def __init__(self, invoice): + self.invoice = invoice + + if 'message' in invoice: + self.message = invoice['message'] + + if 'errors' in invoice: + self.errors = invoice['errors'] + + if 'success' in invoice: + self.success = invoice['success'] + + if 'invoice_id' in invoice: + self.invoice_id = invoice['invoice_id'] + + def is_success(self): + try: + if self.success == True: + return True + except: + pass + + return False + + +class Token(object): + + """ + + This class is representation of payment method to API. + + """ + def __init__(self, token_data): + self.token_data = token_data + + if 'id' in token_data: + self.id = token_data['id'] + if 'extra_info' in token_data: + self.extra_info = token_data['extra_info'] + if 'method' in token_data: + self.method = token_data['method'] + + @property + def is_test(self): + if 'test' in list(self.token_data.keys()) and self.token_data['test'] == True: + return True + else: + return False + + @property + def status(self): + try: + if 'errors' in list(self.token_data.keys()): + return self.token_data['errors'] + except: + pass + + return 200 + + +class Payer(object): + + def __init__(self, name, email, address=None, cpf_cnpj=None, phone_prefix=None, phone=None): + self.cpf_cnpj = cpf_cnpj + self.name = name + self.email = email + self.phone_prefix = phone_prefix + self.phone = phone + + if isinstance(address, Address): + self.address = address + + +class Address(object): + + def __init__(self, street, number, city, state, country, zip_code): + self.street = street + self.number = number + self.city = city + self.state = state + self.country = country + self.zip_code = zip_code + + +class Item(object): + """ + This class represent a checkout item. It's used to create a charge, mainly, + within IuguMerchant class. + """ + + def __init__(self, description, quantity, price_cents, **kwargs): + self.description = description + self.quantity = quantity + self.price_cents = price_cents # must be integer 10.90 => 1090 + self.id = kwargs.get("id") + self.created_at = kwargs.get("created_at") + self.updated_at = kwargs.get("updated_at") + self.price = kwargs.get("price") + #useful for subscriptions subitems + self.recurrent = kwargs.get("recurrent") # boolean + self.total = kwargs.get("total") + # command for eliminate an item + self.destroy = None + + def __str__(self): + return "%s" % self.description + + def to_data(self, is_subscription=False): + """ + Returns tuples to encode with urllib.urlencode + """ + as_tuple = [] + key = "items" + + if is_subscription is True: + key = "subitems" # run to adapt the API subscription + + if self.id: + as_tuple.append(("{items}[][id]".format(items=key), self.id)) + + as_tuple.append(("{items}[][description]".format(items=key), + self.description)) + as_tuple.append(("{items}[][quantity]".format(items=key), + self.quantity)) + as_tuple.append(("{items}[][price_cents]".format(items=key), + self.price_cents)) + + if self.recurrent: + value_recurrent = str(self.recurrent) + value_recurrent = value_recurrent.lower() + as_tuple.append(("{items}[][recurrent]".format(items=key), + value_recurrent)) + + if self.destroy is not None: + value_destroy = str(self.destroy) + value_destroy = value_destroy.lower() + as_tuple.append(("{items}[][_destroy]".format(items=key), + value_destroy)) + + return as_tuple + + def remove(self): + """ + Marks the item that will removed after save an invoice + """ + self.destroy = True + + +class Transfers(object): + + __conn = base.IuguRequests() + __urn = "/v1/transfers" + + def __init__(self, **kwargs): + self.id = kwargs.get("id") + self.created_at = kwargs.get("created_at") + self.amount_cents = kwargs.get("amount_cents") + self.amount_localized = kwargs.get("amount_localized") + self.receiver = kwargs.get("receiver") + self.sender = kwargs.get("sender") + + def send(self, receiver_id, amount_cents): + """ + To send amount_cents to receiver_id + """ + data =[] + data.append(("receiver_id", receiver_id)) + data.append(("amount_cents", amount_cents)) + response = self.__conn.post(self.__urn, data) + return Transfers(**response) + + @classmethod + def getitems(self): + """ + Gets sent and received transfers for use in API_KEY + """ + response = self.__conn.get(self.__urn, []) + sent = response["sent"] + received = response["received"] + transfers = [] + + for t in sent: + transfer_obj = Transfers(**t) + transfers.append(transfer_obj) + + for r in received: + transfer_obj = Transfers(**r) + transfers.append(transfer_obj) + + return transfers diff --git a/lib/iugu/plans.py b/lib/iugu/plans.py new file mode 100644 index 0000000..5c1ed60 --- /dev/null +++ b/lib/iugu/plans.py @@ -0,0 +1,404 @@ +__author__ = 'horacioibrahim' + +# python-iugu package modules +from . import base, config, errors + +class IuguPlan(object): + + """ + + This class allows handling plans. Basically contains a CRUD + + :attribute data: is a descriptor and their setters carries the rules + + => http://iugu.com/referencias/api#criar-um-plano + + """ + + __conn = base.IuguRequests() + + def __init__(self, **kwargs): + self.id = kwargs.get("id") + self.name = kwargs.get("name") + self.identifier = kwargs.get("identifier") + self.interval = kwargs.get("interval") + self.interval_type = kwargs.get("interval_type") + self.created_at = kwargs.get("created_at") + self.updated_at = kwargs.get("updated_at") + self.currency = kwargs.get("currency") # API move it to prices scope + self.value_cents = kwargs.get("value_cents") # API move it to prices scope + self._data = None + self._prices = kwargs.get("prices") + self.prices = [] + self._features = kwargs.get("features") + self.features = [] + + if isinstance(self._prices, list): + for price in self._prices: + obj_price = Price(**price) + self.prices.append(obj_price) + + if isinstance(self._features, list): + for feature in self._features: + obj_feature = Feature(**feature) + self.features.append(obj_feature) + + def is_valid(self): + """Checks required fields to send to API. + + IMPORTANT: Only to use before send request for API. The fields currency + and value_cents will saved in prices scope. Because not to use validate + with returned data by API. + """ + + if self.name and self.identifier and self.interval and \ + self.interval_type and self.currency and self.value_cents: + return True + else: + return False + + @property + def data(self): + return self._data + + @data.setter + def data(self, kwargs): + """Defines data and validates required fields to send to API. + Returns data as list for urlencoded. + """ + data = [] + + # required fields + self.name = kwargs.get("name") + self.identifier = kwargs.get("identifier") + self.interval = kwargs.get("interval") + self.interval_type = kwargs.get("interval_type") + self.currency = kwargs.get("currency") + self.value_cents = kwargs.get("value_cents") + # optional fields + self.prices = kwargs.get("prices") + self.features = kwargs.get("features") + + # required fields. if not passed the API return an exception + if self.name: + data.append(("name", self.name)) + + if self.identifier: + data.append(("identifier", self.identifier)) + + if self.interval: + data.append(("interval", self.interval)) + + if self.interval_type: + data.append(("interval_type", self.interval_type)) + + if self.currency: + if self.currency == "BRL": + data.append(("currency", self.currency)) + else: + raise errors.IuguPlansException(value="Only BRL supported") + + if self.value_cents: + data.append(("value_cents", self.value_cents)) + + # optional fields + if self.prices: + if isinstance(self.prices, list): + # each prices items must be instance's Price class + for price in self.prices: + data.extend(price.to_data()) + else: + raise errors.IuguPlansException(value="The fields prices must "\ + "be a list of obj Price") + + if self.features: + if isinstance(self.features, list): + for feature in self.features: + data.extend(feature.to_data()) + else: + raise errors.IuguPlansException(value="The fields features " \ + "must be a list of obj Feature") + + self._data = data + + @data.deleter + def data(self): + del self._data + + def create(self, name=None, identifier=None, interval=None, + interval_type=None, currency=None, value_cents=None, + features=None, prices=None): + """ + Creates a new plans in API and returns an IuguPlan's instance. The + fields required are name, identifier, interval, interval_type and + values_cents. + + :param name: name of a plan + :param identifier: unique name identifier in API plan context + :param interval: an integer that define duration (e.g 12 to one year) + :param interval_type: a string with "weeks" or "months" + :param currency: only support BRL. If different raise exception + :param value_cents: an integer with price in cents (e.g 1000 > 10.00) + :param prices: a list of prices. The definition in API is obscure + :param features: details with features that must be a list with + instance of Features + """ + urn = "/v1/plans" + + if not name: + if self.name: + name = self.name + else: + raise errors.IuguPlansException(value="Name is required") + + if not identifier: + if self.identifier: + identifier = self.identifier + else: + raise errors.IuguPlansException(value="identifier is required") + + if not interval: + if self.interval: + interval = self.interval + else: + raise errors.IuguPlansException(value="interval is required") + + if not interval_type: + if self.interval_type: + interval_type = self.interval_type + else: + raise errors.IuguPlansException(value="interval_type is required") + + if not features: + if self.features: + features = self.features + + if not prices: + if self.prices: + prices = self.prices + + if not value_cents: + if self.value_cents: + value_cents = self.value_cents + else: + raise errors.IuguPlansException(value="value_cents is required") + + if not currency: + if self.currency: + currency = self.currency + + kwargs_local = locals().copy() + kwargs_local.pop('self') # prevent error of multiple value for args + self.data = kwargs_local + response = self.__conn.post(urn, self.data) + + return IuguPlan(**response) + + def set(self, plan_id, name=None, identifier=None, interval=None, + interval_type=None, currency=None, value_cents=None, + features=None, prices=None): + """ + Edits/changes existent plan and returns IuguPlan's instance + + :param plan_id: ID number of a existent plan + """ + urn = "/v1/plans/{plan_id}".format(plan_id=plan_id) + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self.__conn.put(urn, self.data) + return IuguPlan(**response) + + def save(self): + """Saves an instance of IuguPlan and return own class instance + modified""" + urn = "/v1/plans/{plan_id}".format(plan_id=self.id) + self.data = self.__dict__ + response = self.__conn.put(urn, self.data) + return IuguPlan(**response) + + @classmethod + def get(self, plan_id): + """Gets one plan based in ID and returns an instance""" + data = [] + urn = "/v1/plans/{plan_id}".format(plan_id=plan_id) + response = self.__conn.get(urn, data) + return IuguPlan(**response) + + @classmethod + def get_by_identifier(self, identifier): + """Gets one plan based in identifier and returns an instance + + :param identifier: it's an unique identifier plan in API + """ + data = [] + urn = "/v1/plans/identifier/{identifier}".format(identifier=identifier) + response = self.__conn.get(urn, data) + return IuguPlan(**response) + + @classmethod + def getitems(self, limit=None, skip=None, query=None, updated_since=None, + sort=None): + """ + Gets plans by API default limited 100. + + :param limit: limits the number of plans returned by API (default + and immutable of API is 100) + :param skip: skips a numbers of plans where more recent insert + ordering. Useful to pagination. + :param query: filters based in value (case insensitive) + :param sort: sorts based in field. Use minus signal to determine the + direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API + :return: list of IuguPlan's instances + """ + data = [] + urn = "/v1/plans/" + + # Set options + if limit: + data.append(("limit", limit)) + + if skip: + data.append(("start", skip)) + + if updated_since: + data.append(("updated_since", updated_since)) + + if query: + data.append(("query", query)) + + # TODO: sort not work fine. Waiting support of API providers + if sort: + assert sort is not str, "sort must be string as -name or name" + + if sort.startswith("-"): + sort = sort[1:] + key = "sortBy[{field}]".format(field=sort) + data.append((key, "desc")) + else: + key = "sortBy[{field}]".format(field=sort) + data.append((key, "asc")) + + plans = self.__conn.get(urn, data) + plans_objects = [] + for plan_item in plans["items"]: + obj_plan = IuguPlan(**plan_item) + plans_objects.append(obj_plan) + + return plans_objects + + def remove(self, plan_id=None): + """ + Removes an instance or passing a plan_id + """ + if plan_id: + to_remove = plan_id + else: + to_remove = self.id + + if not to_remove: + raise errors.IuguPlansException(value="Instance or plan id is required") + + urn = "/v1/plans/{plan_id}".format(plan_id=to_remove) + response = self.__conn.delete(urn, []) + # check if result can to generate instance of IuguPlan + obj = IuguPlan(**response) + + if obj: + for k, v in list(self.__dict__.items()): + self.__dict__[k] = None + + +class Price(object): + + """ + + This class is useful for handling field prices of API. Prices in API is a + field of plans context it contains list of values with some fields + exclusively returned by API. + + :method is_valid: check if required fields are correct + :method to_data: returns a list of tuples for urlencoded + + """ + + def __init__(self, **kwargs): + self.id = kwargs.get("id") + self.plan_id = kwargs.get("plan_id") + self.created_at = kwargs.get("created_at") + self.updated_at = kwargs.get("updated_at") + self.value_cents = kwargs.get("value_cents") + self.currency = kwargs.get("currency") + + def is_valid(self): + """Required fields to send to API""" + if self.value_cents and self.currency: + return True + else: + return False + + def to_data(self): + """ + Returns a list of tuples with ("prices[field]", value). Use it to + return a data that will extend the data params in request. + """ + if not self.is_valid(): + blanks = [ k for k, v in list(self.__dict__.items()) if v is None] + raise TypeError("All fields are required to %s. Blanks fields given %s" % + (self.__class__, blanks)) + + data = [] + for k, v in list(self.__dict__.items()): + if v is not None: + key = "prices[][{key_name}]".format(key_name=k) + data.append((key, v)) + + return data + + +class Feature(object): + + """ + + This class abstract features of Plan context. + + :method is_valid: check if required fields are correct + :method to_data: returns a list of tuples for urlencoded + """ + + def __init__(self, **kwargs): + self.id = kwargs.get("id") + self.identifier = kwargs.get("identifier") + self.important = kwargs.get("important") + self.name = kwargs.get("name") + self.plan_id = kwargs.get("plan_id") + self.position = kwargs.get("position") + self.created_at = kwargs.get("created_at") + self.updated_at = kwargs.get("updated_at") + self.value = kwargs.get("value") + + def is_valid(self): + """ + Required to send to API + """ + if self.name and self.identifier and self.value > 0: + return True + else: + return False + + def to_data(self): + """ + Returns a list of tuples with ("features[field]", value). Use it to + return a data that will extend the data params in request. + """ + if not self.is_valid(): + blanks = [ k for k, v in list(self.__dict__.items()) if v is None ] + raise TypeError("All fields are required to class %s. Blanks fields given %s" % + (self.__class__, blanks)) + + data = [] + for k, v in list(self.__dict__.items()): + if v is not None: + key = "features[][{key_name}]".format(key_name=k) + data.append((key, v)) + return data diff --git a/lib/iugu/subscriptions.py b/lib/iugu/subscriptions.py new file mode 100644 index 0000000..6da013f --- /dev/null +++ b/lib/iugu/subscriptions.py @@ -0,0 +1,548 @@ +__author__ = 'horacioibrahim' + +# python-iugu package modules +from . import base, config, errors, merchant + +class IuguSubscription(base.IuguApi): + + """ + + This class allows handling subscriptions an CRUD with create, get, set, + save and remove plus add-ons as getitems, suspend, activate, change_plan, + is_credit_based. + + :attribute class data: is a description it carries rules of data to API + """ + + _conn = base.IuguRequests() + + def __init__(self, **kwargs): + super(IuguSubscription, self).__init__(**kwargs) + self.id = kwargs.get("id") + # required + self.customer_id = kwargs.get("customer_id") + # optionals + self.plan_identifier = kwargs.get("plan_identifier") # only credits_based subscriptions + self.expires_at = kwargs.get("expires_at") + # self.only_on_charge_success = kwargs.get("only_on_charge_success") # if exist payment method for client + self._subitems = kwargs.get("subitems") + self.subitems = [] # of items + self.custom_variables = kwargs.get("custom_variables") + self._data = None + self.suspended = kwargs.get("suspended") + self.price_cents = kwargs.get("price_cents") + self.currency = kwargs.get("currency") + # created by api + self.created_at = kwargs.get("created_at") + self.updated_at = kwargs.get("updated_at") + self.customer_name = kwargs.get("customer_name") + self.customer_email = kwargs.get("customer_email") + self.cycled_at = kwargs.get("cycled_at") + self.plan_name = kwargs.get("plan_name") + self.customer_ref = kwargs.get("customer_ref") + self.plan_ref = kwargs.get("plan_ref") + self.active = kwargs.get("active") + self.in_trial = kwargs.get("in_trial") + self.recent_invoices = kwargs.get("recent_invoices") # only resume of invoice + self.logs = kwargs.get("logs") + self._type = kwargs.get("_type") # facilities to verify if credit_base or general + + if isinstance(self._subitems, list): + for item in self._subitems: + obj_item = merchant.Item(**item) + self.subitems.append(obj_item) + + @staticmethod + def is_credit_based(response): + # Checks if HTTP response of API subscription is credit_based type + if "credits_based" in response and response["credits_based"] == True: + return True + return False + + @property + def data(self): + return self._data + + @data.setter + def data(self, kwargs): + """ + Body data for request send + """ + data = [] + self.id = kwargs.get("sid") + self.customer_id = kwargs.get("customer_id") + self.plan_identifier = kwargs.get("plan_identifier") + self.expires_at = kwargs.get("expires_at") + self.only_on_charge_success = kwargs.get("only_on_charge_success") + self.subitems = kwargs.get("subitems") + self.custom_variables = kwargs.get("custom_data") + self.credits_based = kwargs.get("credits_based") + self.credits_min = kwargs.get("credits_min") + self.credits_cycle = kwargs.get("credits_cycle") + self.price_cents = kwargs.get("price_cents") + self.suspended = kwargs.get("suspended") + self.skip_charge = kwargs.get("skip_charge") + + if self.id: + data.append(("id", self.id)) + + if self.customer_id: + data.append(("customer_id", self.customer_id)) + + if self.plan_identifier: + data.append(("plan_identifier", self.plan_identifier)) + + if self.expires_at: + data.append(("expires_at", self.expires_at)) + + if self.only_on_charge_success: + value_charge_success = str(self.only_on_charge_success) + value_charge_success = value_charge_success.lower() + data.append(("only_on_charge_success", value_charge_success)) + + if self.subitems: + if isinstance(self.subitems, list): + for item in self.subitems: + data.extend((item.to_data(is_subscription=True))) + else: + raise errors.IuguSubscriptionsException("The subitems must be " \ + "a list of obj Item") + + if self.custom_variables: # TODO: to create test + data.extend(self.custom_variables) + + # credit based subscriptions + if self.credits_based is not None: + value_credits_based = str(self.credits_based) + value_credits_based = value_credits_based.lower() + data.append(("credits_based", value_credits_based)) + + if self.credits_min: + data.append(("credits_min", self.credits_min)) + + if self.credits_cycle: + data.append(("credits_cycle", self.credits_cycle)) + + if self.price_cents: + data.append(("price_cents", self.price_cents)) + + if self.suspended is not None: + value_suspended = str(self.suspended) + value_suspended = value_suspended.lower() + data.append(("suspended", value_suspended)) + + if self.skip_charge is not None: + value_skip_charge = str(self.skip_charge) + value_skip_charge = value_skip_charge.lower() + data.append(("skip_charge", value_skip_charge)) + + self._data = data + + @data.deleter + def data(self): + del self._data + + def create(self, customer_id, plan_identifier, expires_at=None, + only_on_charge_success=False, subitems=None, + custom_variables=None): + """ + Creates new subscription + + :param customer_id: the ID of an existent customer + :param plan_identifier: the identifier of a plan (it's not ID) + :param expires_at: a string with expiration date and next charge (e.g + "DD/MM/YYYY" or "31/12/2014") + :param only_on_charge_success: creates the subscriptions if charged + with success. It's supported if customer already have payment method + inserted + :param subitems: items of subscriptions + + => http://iugu.com/referencias/api#criar-uma-assinatura + """ + urn = "/v1/subscriptions" + if custom_variables: + assert isinstance(custom_variables, dict), "Required a dict" + custom_data = self.custom_variables_list(custom_variables) + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self._conn.post(urn, self.data) + return IuguSubscription(**response) + + def set(self, sid, plan_identifier=None, expires_at=None, + subitems=None, suspended=None, skip_charge=None, + custom_variables=None, customer_id=None): + """ + Changes a subscriptions with based arguments and Returns modified + subscription of type no credit_based. + + :param sid: ID of an existent subscriptions in API + :param customer_id: ID of customer + :param expires_at: expiration date and date of next charge + :param subitems: subitems + :param suspended: boolean to change status of subscription + :param skip_charge: ignore charge. Bit explanation and obscure in API + :param custom_variables: a dictionary {'key': 'value'} + + IMPORTANT 1: Removed parameter customer_id. Iugu's support (number 782) + says that to change only customer_id isn't supported by API. + """ + urn = "/v1/subscriptions/{sid}".format(sid=sid) + if custom_variables: + assert isinstance(custom_variables, dict), "Required a dict" + custom_data = self.custom_variables_list(custom_variables) + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self._conn.put(urn, self.data) + response["_type"] = "general" + return IuguSubscription(**response) + + def save(self): + """Saves an instance of subscription and return own class instance + modified""" + + if self.id: + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="Save is support "\ + "only to returned API object.") + + kwargs = {} + # TODO: to improve this ineffective approach + # Currently this check if the required set's parameters was passed + # If changes occurs in set() to revise this k in if used to mount kwargs + for k, v in list(self.__dict__.items()): + if v is not None: + if k == "plan_identifier" or \ + k == "expires_at" or k == "subitems" or \ + k == "suspended" or k == "skip_charge" or \ + k == "custom_variables": + kwargs[k] = v + last_valid_k = k + + if isinstance(v, list) and len(v) == 0 and last_valid_k: + # solves problem with arguments of empty lists + del kwargs[last_valid_k] + + return self.set(sid, **kwargs) + + @classmethod + def get(self, sid): + """ + Fetch one subscription based in ID and returns one of two's types of + subscriptions: credit_based or no credit_based + + :param sid: ID of an existent subscriptions in API + """ + urn = "/v1/subscriptions/{sid}".format(sid=sid) + response = self._conn.get(urn, []) + + if self.is_credit_based(response): + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + response["_type"] = "general" + return IuguSubscription(**response) + + @classmethod + def getitems(self, limit=None, skip=None, created_at_from=None, + created_at_to=None, query=None, updated_since=None, sort=None, + customer_id=None): + """ + Gets subscriptions by API default limited 100. + """ + data = [] + urn = "/v1/subscriptions/" + + # Set options + if limit: + data.append(("limit", limit)) + if skip: + data.append(("start", skip)) + if created_at_from: + data.append(("created_at_from", created_at_from)) + if created_at_to: + data.append(("created_at_to", created_at_to)) + if updated_since: + data.append(("updated_since", updated_since)) + if query: + data.append(("query", query)) + if customer_id: + data.append(("customer_id", customer_id)) + + # TODO: sort not work fine. Waiting support of API providers + if sort: + assert sort is not str, "sort must be string as -name or name" + + if sort.startswith("-"): + sort = sort[1:] + key = "sortBy[{field}]".format(field=sort) + data.append((key, "desc")) + else: + key = "sortBy[{field}]".format(field=sort) + data.append((key, "asc")) + + subscriptions = self._conn.get(urn, data) + subscriptions_objs = [] + for s in subscriptions["items"]: + # add items in list but before verifies if credit_based + if self.is_credit_based(s): + s["_type"] = "credit_based" + obj_subscription = SubscriptionCreditsBased(**s) + else: + s["_type"] = "general" + obj_subscription = IuguSubscription(**s) + + subscriptions_objs.append(obj_subscription) + + return subscriptions_objs + + def remove(self, sid=None): + """ + Removes a subscription given id or instance + + :param sid: ID of an existent subscriptions in API + """ + if not sid: + if self.id: + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}".format(sid=sid) + self._conn.delete(urn, []) + + def suspend(self, sid=None): + """ + Suspends an existent subscriptions + + :param sid: ID of an existent subscriptions in API + """ + if not sid: + if self.id: + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}/suspend".format(sid=sid) + response = self._conn.post(urn, []) + + if self.is_credit_based(response): + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + response["_type"] = "general" + return IuguSubscription(**response) + + def activate(self, sid=None): + """ + Activates an existent subscriptions + + :param sid: ID of an existent subscriptions in API + + NOTE: This option not work fine by API + """ + if not sid: + if self.id: + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}/activate".format(sid=sid) + response = self._conn.post(urn, []) + + if self.is_credit_based(response): + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + response["_type"] = "general" + return IuguSubscription(**response) + + def change_plan(self, plan_identifier, sid=None): + """ + Changes the plan for existent subscriptions + + :param sid: ID of an existent subscriptions in API + :param plan_identifier: the identifier of a plan (it's not ID) + """ + if not sid: + if self.id: + # short-circuit + if "credits_based" in self.__dict__ and self.credits_based: + raise errors.\ + IuguSubscriptionsException(value="Instance must be " \ + "object of IuguSubscriptionsException") + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}/change_plan/{plan_identifier}"\ + .format(sid=sid, plan_identifier=plan_identifier) + response = self._conn.post(urn, []) + + if self.is_credit_based(response): + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + response["_type"] = "general" + return IuguSubscription(**response) + + +class SubscriptionCreditsBased(IuguSubscription): + + """ + + This class make additional approaches for subscriptions based in credits. + Addition methods as add_credits and remove_credits. + + :method create: it has parameters different of class extended + :method set: it has parameters different of class extended + + """ + + def __init__(self, **kwargs): + super(SubscriptionCreditsBased, self).__init__(**kwargs) + self.credits_based = True + self.credits_cycle = kwargs.get("credits_cycle") + self.credits_min = kwargs.get("credits_min") + self.credits = kwargs.get("credits") + + def create(self, customer_id, credits_cycle, price_cents=None, + credits_min=None, expires_at=None, only_on_charge_success=None, + subitems=None, custom_variables=None): + """ + Create a subscription based in credits and return the instance + this class. + + :param: custom_variables: a dict {'key': 'value'} + """ + + if price_cents is None or price_cents <= 0: + raise errors.IuguSubscriptionsException(value="price_cents must be " \ + "greater than 0") + + credits_based = self.credits_based + + if custom_variables: + assert isinstance(custom_variables, dict), "Required a dict" + custom_data = self.custom_variables_list(custom_variables) + + kwargs_local = locals().copy() + kwargs_local.pop('self') + urn = "/v1/subscriptions" + self.data = kwargs_local + response = self._conn.post(urn, self.data) + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + def set(self, sid, expires_at=None, subitems=None, suspended=None, + skip_charge=None, price_cents=None, credits_cycle=None, + credits_min=None, custom_variables=None): + """ + Changes an existent subscription no credit_based + + :param sid: ID of an existent subscriptions in API + """ + urn = "/v1/subscriptions/{sid}".format(sid=sid) + credits_based = self.credits_based + if custom_variables: + assert isinstance(custom_variables, dict), "Required a dict" + custom_data = self.custom_variables_list(custom_variables) + kwargs_local = locals().copy() + kwargs_local.pop('self') + self.data = kwargs_local + response = self._conn.put(urn, self.data) + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + def save(self): + """ Saves an instance of this class that was persisted or raise error + if no instance. + + NOTE: to use create() or set() for add/change custom_variables + """ + if self.id: + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="Save is support "\ + "only to returned API object.") + + kwargs = {} + # TODO: to improve this ineffective approach. + # Currently this check if the set's parameters was passed. If changes + # occurs in set() to revise this k in if used to mount kwargs + for k, v in list(self.__dict__.items()): + if v is not None: + if k == "expires_at" or \ + k == "subitems" or k == "suspended" or \ + k == "skip_charge" or k == "price_cents" or \ + k == "credits_cycle" or k == "credits_min" or \ + k == "custom_variables" : + kwargs[k] = v + last_valid_k = k + + if isinstance(v, list) and len(v) == 0 and last_valid_k: + # solves problem with arguments of empty lists + del kwargs[last_valid_k] + del last_valid_k + + return self.set(sid, **kwargs) + + def add_credits(self, quantity, sid=None): + """ + Adds credits in existent subscriptions + + :param sid: ID of an existent subscriptions in API + :param plan_identifier: the identifier of a plan (it's not ID) + """ + data = [] + if not sid: + if self.id: + if not self.credits_based: + raise errors.\ + IuguSubscriptionsException(value="Instance must be " \ + "object of SubscriptionCreditsBased") + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}/add_credits".format(sid=sid) + data.append(("quantity", quantity)) + response = self._conn.put(urn, data) + + if not self.is_credit_based(response): + raise errors.IuguSubscriptionsException(value="Instance must be " \ + "object of SubscriptionCreditsBased") + + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) + + def remove_credits(self, quantity, sid=None): + """ + Suspends an existent subscriptions + + :param sid: ID of an existent subscriptions in API + :param plan_identifier: the identifier of a plan (it's not ID) + """ + data = [] + if not sid: + if self.id: + if not self.credits_based: + raise errors.\ + IuguSubscriptionsException(value="Instance must be " \ + "object of SubscriptionCreditsBased") + sid = self.id + else: + raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") + + urn = "/v1/subscriptions/{sid}/remove_credits".format(sid=sid) + data.append(("quantity", quantity)) + response = self._conn.put(urn, data) + + if not self.is_credit_based(response): + raise errors.IuguSubscriptionsException(value="Instance must be " \ + "object of SubscriptionCreditsBased") + + response["_type"] = "credit_based" + return SubscriptionCreditsBased(**response) diff --git a/lib/iugu/tests.py b/lib/iugu/tests.py new file mode 100644 index 0000000..145dde9 --- /dev/null +++ b/lib/iugu/tests.py @@ -0,0 +1,1785 @@ +#coding: utf-8 + +__author__ = 'horacioibrahim' + +import unittest, os +import datetime +from time import sleep, ctime, time +from random import randint +from hashlib import md5 +from types import StringType + +# python-iugu package modules +from . import merchant, customers, config, invoices, errors, plans, subscriptions + + +def check_tests_environment(): + """ + For tests is need environment variables to instantiate merchant. Or + Edit tests file to instantiate merchant.IuguMerchant(account_id=YOUR_ID) + """ + try: + global ACCOUNT_ID + ACCOUNT_ID = os.environ["ACCOUNT_ID"] + except KeyError: + raise errors.IuguConfigTestsErrors("Only for tests is required an environment " \ + "variable ACCOUNT_ID or edit file tests.py") + +class TestMerchant(unittest.TestCase): + + check_tests_environment() # Checks if enviroment variables defined + def setUp(self): + self.EMAIL_CUSTOMER = "anyperson@ap.com" + self.client = merchant.IuguMerchant(account_id=ACCOUNT_ID, + api_mode_test=True) + + def tearDown(self): + pass + + def test_create_payment_token_is_test(self): + response = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', + '12', '2010', '123') + self.assertTrue(response.is_test) + + def test_create_payment_token(self): + response = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', + '12', '2010', '123') + self.assertEqual(response.status, 200) + + def test_create_charge_credit_card(self): + item = merchant.Item("Produto My Test", 1, 10000) + token = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', + '12', '2010', '123') + charge = self.client.create_charge(self.EMAIL_CUSTOMER, item, token=token) + self.assertEqual(charge.is_success(), True) + + def test_create_charge_bank_slip(self): + item = merchant.Item("Produto Bank Slip", 1, 1000) + charge = self.client.create_charge(self.EMAIL_CUSTOMER, item) + self.assertEqual(charge.is_success(), True) + + +class TestCustomer(unittest.TestCase): + + def setUp(self): + hash_md5 = md5() + number = randint(1, 50) + hash_md5.update(str(number)) + email = "{email}@test.com".format(email=hash_md5.hexdigest()) + self.random_user_email = email + + + def test_create_customer_basic_info(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + c = consumer.create() + c.remove() + self.assertEqual(consumer.email, c.email) + + def test_create_customer_basic_email(self): + consumer = customers.IuguCustomer() + c = consumer.create(email=self.random_user_email) + c.remove() + self.assertEqual(consumer.email, c.email) + + def test_create_customer_extra_attrs(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + c = consumer.create(name="Mario Lago", notes="It's the man", + custom_variables={'local':'cup'}) + c.remove() + self.assertEqual(c.custom_variables[0]['name'], "local") + self.assertEqual(c.custom_variables[0]['value'], "cup") + + def test_get_customer(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + consumer_new = consumer.create() + c = consumer.get(customer_id=consumer_new.id) + consumer_new.remove() + self.assertEqual(consumer.email, c.email) + + def test_set_customer(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + consumer_new = consumer.create(name="Mario Lago", notes="It's the man", + custom_variables={'local':'cup'}) + c = consumer.set(consumer_new.id, name="Lago Mario") + consumer_new.remove() + self.assertEqual(c.name, "Lago Mario") + + def test_customer_save(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + consumer_new = consumer.create(name="Mario Lago", notes="It's the man", + custom_variables={'local':'cup'}) + + # Edit info + consumer_new.name = "Ibrahim Horacio" + # Save as instance + consumer_new.save() + # verify results + check_user = consumer.get(consumer_new.id) + consumer_new.remove() + self.assertEqual(check_user.name, "Ibrahim Horacio") + + def test_customer_delete_by_id(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + consumer_new = consumer.create(name="Mario Lago", notes="It's the man", + custom_variables={'local':'cup'}) + consumer.delete(consumer_new.id) + self.assertRaises(errors.IuguGeneralException, consumer.get, + consumer_new.id) + + def test_customer_delete_instance(self): + consumer = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + consumer_new = consumer.create(name="Mario Lago", notes="It's the man", + custom_variables={'local':'cup'}) + + r = consumer_new.remove() + self.assertRaises(errors.IuguGeneralException, consumer.get, + consumer_new.id) + + +class TestCustomerLists(unittest.TestCase): + + def setUp(self): + hash_md5 = md5() + number = randint(1, 50) + hash_md5.update(str(number)) + email = "{email}@test.com".format(email=hash_md5.hexdigest()) + self.random_user_email = email + + self.c = customers.IuguCustomer(api_mode_test=True, + email=self.random_user_email) + + # creating customers for tests with lists + p1, p2, p3 = "Andrea", "Bruna", "Carol" + self.one = self.c.create(name=p1, notes="It's the man", + custom_variables={'local':'cup'}) + + # I'm not happy with it (sleep), but was need. This certainly occurs because + # time data is not a timestamp. + sleep(1) + self.two = self.c.create(name=p2, notes="It's the man", + custom_variables={'local':'cup'}) + sleep(1) + self.three = self.c.create(name=p3, notes="It's the man", + custom_variables={'local':'cup'}) + sleep(1) + + self.p1, self.p2, self.p3 = p1, p2, p3 + + def tearDown(self): + self.one.remove() + self.two.remove() + self.three.remove() + + def test_getitems(self): + customers_list = self.c.getitems() + self.assertEqual(type(customers_list), list) + + def test_getitems_limit(self): + # get items with auto DESC order + customers_list = self.c.getitems(limit=2) + self.assertEqual(len(customers_list), 2) + + def test_getitems_start(self): + # get items with auto DESC order + sleep(2) + customers_list = self.c.getitems(limit=3) # get latest three customers + reference_customer = customers_list[2].name + customers_list = self.c.getitems(skip=2) + self.assertEqual(customers_list[0].name, reference_customer) + + def test_getitems_query_by_match_in_name(self): + hmd5 = md5() + hmd5.update(ctime(time())) + salt = hmd5.hexdigest() + term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) + + # test value/term in >>name<< + customer = self.c.create(name=term) + sleep(2) + items = self.c.getitems(query=term) # assert valid because name + customer.remove() + self.assertEqual(items[0].name, term) + + def test_getitems_query_by_match_in_notes(self): + hmd5 = md5() + hmd5.update(ctime(time())) + salt = hmd5.hexdigest() + term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) + # test value/term in >>notes<< + customer = self.c.create(name="Sub Zero", notes=term) + sleep(2) + items = self.c.getitems(query=term) + customer.remove() + self.assertEqual(items[0].notes, term) + + def test_getitems_query_by_match_in_email(self): + hmd5 = md5() + hmd5.update(ctime(time())) + salt = hmd5.hexdigest() + term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) + # test value/term in >>email<< + email = term + '@email.com' + self.c.email = email + customer = self.c.create() + sleep(2) + items = self.c.getitems(query=term) + customer.remove() + self.assertIn(term, items[0].email) + + # Uncomment/comment the next one line to disable/enable the test + @unittest.skip("Database of webservice is not empty") + def test_getitems_sort(self): + sleep(1) # Again. It's need + # Useful to test database with empty data (previous data, old tests) + customers_list = self.c.getitems(sort="name") + + # monkey skip + if len(customers_list) < 4: + self.assertEqual(customers_list[0].name, self.p1) + else: + raise TypeError("API Database is not empty. This test isn't useful. " \ + "Use unittest.skip() before this method.") + + def test_getitems_created_at_from(self): + sleep(1) + customers_list = self.c.getitems(created_at_from=self.three.created_at) + self.assertEqual(customers_list[0].id, self.three.id) + + # Uncomment the next one line to disable the test + # @unittest.skip("Real-time interval not reached") + def test_getitems_created_at_to(self): + sleep(1) + customers_list = self.c.getitems(created_at_to=self.one.created_at) + self.assertEqual(customers_list[0].id, self.three.id) + + def test_getitems_updated_since(self): + # get items with auto DESC order + sleep(1) + customers_list = self.c.getitems(updated_since=self.three.created_at) + self.assertEqual(customers_list[0].id, self.three.id) + + +class TestCustomerPayments(unittest.TestCase): + + def setUp(self): + hash_md5 = md5() + number = randint(1, 50) + hash_md5.update(str(number)) + email = "{email}@test.com".format(email=hash_md5.hexdigest()) + self.random_user_email = email + self.client = customers.IuguCustomer(email="test@testmail.com") + self.customer = self.client.create() + + self.instance_payment = self.customer.payment.create(description="New payment method", + number='4111111111111111', + verification_value=123, + first_name="Joao", last_name="Maria", + month=12, year=2014) + + def tearDown(self): + self.instance_payment.remove() + self.customer.remove() # if you remove customer also get payment + + def test_create_payment_method_new_user_by_create(self): + """ Test create payment method to new recent user returned by create() + of IuguCustomer + """ + instance_payment = self.customer.payment.create(description="New payment method", + number='4111111111111111', + verification_value=123, + first_name="Joao", last_name="Maria", + month=12, year=2014) + instance_payment.remove() + self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) + + def test_create_payment_method_existent_user_by_get(self): + """ Test create payment method of existent user returned by get() + of IuguCustomer. + """ + new_customer = self.client.create() + # Test with user from get() + existent_customer = self.client.get(new_customer.id) + + instance_payment = existent_customer.payment.create(description="New payment method", + number='4111111111111111', + verification_value=123, + first_name="Joao", last_name="Maria", + month=12, year=2015) + instance_payment.remove() + self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) + + def test_create_payment_method_existent_user_by_getitems(self): + """ Test create payment method of existent user returned by getitems() + of IuguCustomer + """ + # Test with user from getitems() + customers_list = self.client.getitems() + c_0 = customers_list[0] + + instance_payment = c_0.payment.create(description="New payment method", + number='4111111111111111', + verification_value=123, + first_name="Joao", last_name="Maria", + month=12, year=2016) + instance_payment.remove() + self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) + + def test_create_payment_method_non_existent_user_by_instance(self): + """ Test create payment method to instance's user before it was + created in API. So without ID. + """ + create = self.client.payment.create + + self.assertRaises(errors.IuguPaymentMethodException, + create, description="New payment method", + number='4111111111111111', + verification_value=123, first_name="Joao", + last_name="Maria", month=12, year=2016) + + def test_create_payment_method_raise_general(self): + # Create payment method without data{} where API returns error. + customer = self.client.create() + self.assertRaises(errors.IuguGeneralException, customer.payment.create, + description="Second payment method") + customer.remove() + + def test_get_payment_method_by_payment_id_customer_id(self): + # Test get payment based payment_id and customer_id + id = self.instance_payment.id + # two args passed + payment = self.client.payment.get(id, customer_id=self.customer.id) + self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) + + def test_get_payment_by_customer(self): + # Test get payment by instance's customer (existent in API) + id = self.instance_payment.id + # one arg passed. user is implicit to customer + payment = self.customer.payment.get(id) + self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) + + def test_set_payment_by_payment_id_customer_id(self): + # Changes payment method base payment_id and customer_id + id = self.instance_payment.id + # two args passed + payment = self.client.payment.set(id, "New Card Name", + customer_id=self.customer.id) + self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) + payment_test = self.customer.payment.get(payment.id) + self.assertEqual(payment_test.description, payment.description) + + def test_set_payment_by_customer(self): + # Changes payment method base payment_id of an intance's customer + id = self.instance_payment.id + # one arg passed. user is implicit to customer + payment = self.customer.payment.set(id, "New Card Name") + self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) + payment_test = self.customer.payment.get(payment.id) + self.assertEqual(payment_test.description, payment.description) + + def test_set_payment_by_customer_by_save(self): + """ Changes payment method of an instance's payment no payment_id or + no customer_id is need""" + self.instance_payment.description = "New Card Name" + # no args passed. To payment method instance this is implicit + payment = self.instance_payment.save() + self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) + payment_test = self.customer.payment.get(payment.id) + self.assertEqual(payment_test.description, payment.description) + + def test_set_payment_remove(self): + """ Changes payment method of an instance's payment no payment_id or + no customer_id is need""" + instance_payment = self.customer.payment.create(description="New payment method", + number='4111111111111111', + verification_value=123, + first_name="Joao", last_name="Maria", + month=12, year=2014) + instance_payment.remove() + # Try get payment already removed + payment_test = self.customer.payment.get # copy method + self.assertRaises(errors.IuguGeneralException, payment_test, + instance_payment.id) + + def test_set_payment_remove_by_attrs(self): + """ + + """ + instance_payment = self.customer.payment + instance_payment.payment_data.description = "New payment method" + instance_payment.payment_data.number = number='4111111111111111' + instance_payment.payment_data.verification_value = 123 + instance_payment.payment_data.first_name = "Joao" + instance_payment.payment_data.last_name = "Silva" + instance_payment.payment_data.month = 12 + instance_payment.payment_data.year = 2015 + instance_payment = instance_payment.create(description="Meu cartao") + instance_payment.remove() + self.assertRaises(errors.IuguGeneralException, instance_payment.get, instance_payment.id) + + def test_getitems_payments(self): + payment_one = self.customer.payment.create(description="New payment One", + number='4111111111111111', + verification_value=123, + first_name="World", last_name="Cup", + month=12, year=2014) + payment_two = self.customer.payment.create(description="New payment Two", + number='4111111111111111', + verification_value=123, + first_name="Is a ", last_name="Problem", + month=12, year=2015) + payment_three = self.customer.payment.create(description="New payment Three", + number='4111111111111111', + verification_value=123, + first_name="To Brazil", last_name="Worry", + month=12, year=2015) + list_of_payments = self.customer.payment.getitems() + self.assertTrue(isinstance(list_of_payments, list)) + self.assertTrue(isinstance(list_of_payments[0], + customers.IuguPaymentMethod)) + + +class TestInvoice(unittest.TestCase): + TODAY = datetime.date.today().strftime("%d/%m/%Y") + check_tests_environment() # Checks if enviroment variables defined + + def setUp(self): + hash_md5 = md5() + number = randint(1, 50) + hash_md5.update(str(number)) + email = "{email}@test.com".format(email=hash_md5.hexdigest()) + self.customer_email = email + # create a customer for tests + c = customers.IuguCustomer() + self.consumer = c.create(email="client@customer.com") + + # create a invoice + item = merchant.Item("Prod 1", 1, 1190) + self.item = item + self.invoice_obj = invoices.IuguInvoice(email=self.customer_email, + item=item, due_date=self.TODAY) + self.invoice = self.invoice_obj.create(draft=True) + + # to tests for refund + self.EMAIL_CUSTOMER = "anyperson@ap.com" + self.client = merchant.IuguMerchant(account_id=ACCOUNT_ID, + api_mode_test=True) + + def tearDown(self): + if self.invoice.id: # if id is None already was removed + self.invoice.remove() + self.consumer.remove() + + def test_invoice_raise_required_email(self): + i = invoices.IuguInvoice() + self.assertRaises(errors.IuguInvoiceException, i.create, + due_date="30/11/2020", items=self.item) + + def test_invoice_raise_required_due_date(self): + i = invoices.IuguInvoice() + self.assertRaises(errors.IuguInvoiceException, i.create, + email="h@gmail.com", items=self.item) + + def test_invoice_raise_required_items(self): + i = invoices.IuguInvoice() + self.assertRaises(errors.IuguInvoiceException, i.create, + due_date="30/11/2020", email="h@gmail.com") + + def test_invoice_create_basic(self): + self.assertTrue(isinstance(self.invoice, invoices.IuguInvoice)) + + def test_invoice_with_customer_id(self): + res = self.invoice_obj.create(customer_id=self.consumer.id) + self.assertEqual(res.customer_id, self.consumer.id) + + def test_invoice_create_all_fields_as_draft(self): + response = self.invoice_obj.create(draft=True, return_url='http://hipy.co/success', + expired_url='http://hipy.co/expired', + notification_url='http://hipy.co/webhooks', + tax_cents=200, discount_cents=500, + customer_id=self.consumer.id, + ignore_due_email=True) + self.assertTrue(isinstance(response, invoices.IuguInvoice)) + existent_invoice = invoices.IuguInvoice.get(response.id) + self.assertEqual(existent_invoice.expiration_url, response.expiration_url) + response.remove() + + def test_invoice_create_all_fields_as_pending(self): + response = self.invoice_obj.create(draft=False, + return_url='http://example.com/success', + expired_url='http://example.com/expired', + notification_url='http://example.com/webhooks', + tax_cents=200, discount_cents=500, + customer_id=self.consumer.id, + ignore_due_email=True) + self.assertTrue(isinstance(response, invoices.IuguInvoice)) + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # response.remove() + + def test_invoice_created_check_id(self): + self.assertIsNotNone(self.invoice.id) + + def test_invoice_create_with_custom_variables_in_create(self): + invoice = self.invoice_obj.create(draft=True, + custom_variables={'city': 'Brasilia'}) + self.assertEqual(invoice.custom_variables[0]["name"], "city") + self.assertEqual(invoice.custom_variables[0]["value"], "Brasilia") + invoice.remove() + + def test_invoice_create_with_custom_variables_in_set(self): + invoice = self.invoice_obj.set(invoice_id=self.invoice.id, + custom_variables={'city': 'Brasilia'}) + self.assertEqual(invoice.custom_variables[0]["name"], "city") + self.assertEqual(invoice.custom_variables[0]["value"], "Brasilia") + + def test_invoice_get_one(self): + # test start here + res = invoices.IuguInvoice.get(self.invoice.id) + self.assertEqual(res.items[0].description, "Prod 1") + + def test_invoice_create_as_draft(self): + self.assertEqual(self.invoice.status, 'draft') + + def test_invoice_edit_email_with_set(self): + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, email="now@now.com") + self.assertEqual(invoice_edited.email, "now@now.com") + + def test_invoice_edit_return_url_with_set(self): + return_url = "http://hipy.co" + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + return_url=return_url) + self.assertEqual(invoice_edited.return_url, return_url) + + @unittest.skip("It isn't support by API") + def test_invoice_edit_expired_url_with_set(self): + expired_url = "http://hipy.co" + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + expired_url=expired_url) + self.assertEqual(invoice_edited.expiration_url, expired_url) + + def test_invoice_edit_notification_url_with_set(self): + notification_url = "http://hipy.co" + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + notification_url=notification_url) + self.assertEqual(invoice_edited.notification_url, notification_url) + + def test_invoice_edit_tax_cents_with_set(self): + tax_cents = 200 + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + tax_cents=tax_cents) + self.assertEqual(invoice_edited.tax_cents, tax_cents) + + def test_invoice_edit_discount_cents_with_set(self): + discount_cents = 500 + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + discount_cents=discount_cents) + self.assertEqual(invoice_edited.discount_cents, discount_cents) + + def test_invoice_edit_customer_id_with_set(self): + customer_id = self.consumer.id + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + customer_id=customer_id) + self.assertEqual(invoice_edited.customer_id, customer_id) + + @unittest.skip("without return from API of the field/attribute ignore_due_email") + def test_invoice_edit_ignore_due_email_with_set(self): + ignore_due_email = True + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + ignore_due_email=ignore_due_email) + self.assertEqual(invoice_edited.ignore_due_email, ignore_due_email) + + # TODO: def test_invoice_edit_subscription_id_with_set(self): + + # TODO: test_invoice_edit_credits_with_set(self): + + def test_invoice_edit_due_date_with_set(self): + due_date = self.TODAY + response_from_api = str(datetime.date.today()) + id = self.invoice.id + invoice_edited = self.invoice_obj.set(invoice_id=id, + due_date=due_date) + self.assertEqual(invoice_edited.due_date, response_from_api) + + def test_invoice_edit_items_with_set(self): + self.invoice.items[0].description = "Prod Fixed Text and Value" + id = self.invoice.id + items = self.invoice.items[0] + invoice_edited = self.invoice_obj.set(invoice_id=id, items=items) + self.assertEqual(invoice_edited.items[0].description, "Prod Fixed Text and Value") + + def test_invoice_changed_items_with_save(self): + self.invoice.items[0].description = "Prod Saved by Instance" + # inv_one is instance not saved. Now, we have invoice saved + # and invoice_edited that is the response of webservice + res = self.invoice.save() + self.assertEqual(res.items[0].description, "Prod Saved by Instance") + + def test_invoice_destroy_item(self): + # Removes one item, the unique, created in invoice + self.invoice.items[0].remove() + re_invoice = self.invoice.save() + self.assertEqual(re_invoice.items, None) + + def test_invoice_remove(self): + # wait webservice response time + sleep(3) + self.invoice.remove() + self.assertEqual(self.invoice.id, None) + + def test_invoice_get_and_save(self): + inv = invoices.IuguInvoice.get(self.invoice.id) + inv.email = "test_save@save.com" + obj = inv.save() + self.assertEqual(obj.email, inv.email) + + def test_invoice_getitems_and_save(self): + sleep(2) # wating...API to persist data + inv = None + invs = invoices.IuguInvoice.getitems() + for i in invs: + if i.id == self.invoice.id: + inv = i + inv.email = "test_save@save.com" + obj = inv.save() + self.assertEqual(obj.email, inv.email) + + def test_invoice_cancel(self): + invoice = self.invoice_obj.create(draft=False) + re_invoice = invoice.cancel() + self.assertEqual(re_invoice.status, "canceled") + invoice.remove() + + #@unittest.skip("Support only invoice paid") # TODO + def test_invoice_refund(self): + item = merchant.Item("Produto My Test", 1, 10000) + token = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', + '12', '2010', '123') + charge = self.client.create_charge(self.EMAIL_CUSTOMER, item, token=token) + invoice = invoices.IuguInvoice.get(charge.invoice_id) + re_invoice = invoice.refund() + self.assertEqual(re_invoice.status, "refunded") + + def test_invoice_getitems(self): + # wait webservice response time + sleep(3) + l = invoices.IuguInvoice.getitems() + self.assertIsInstance(l, list) + self.assertIsInstance(l[0], invoices.IuguInvoice) + + def test_invoice_getitems_limit(self): + invoice_2 = self.invoice_obj.create() + sleep(3) + l = invoices.IuguInvoice.getitems(limit=2) + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # invoice_2.remove() + self.assertEqual(len(l), 2) + + def test_invoice_getitems_skip(self): + invoice_1 = self.invoice_obj.create() + invoice_2 = self.invoice_obj.create() + invoice_3 = self.invoice_obj.create() + sleep(3) + l1 = invoices.IuguInvoice.getitems(limit=3) + keep_checker = l1[2] + l2 = invoices.IuguInvoice.getitems(skip=2) + skipped = l2[0] # after skip 2 the first must be keep_checker + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # invoice_1.remove() + # invoice_2.remove() + # invoice_3.remove() + self.assertEqual(keep_checker.id, skipped.id) + + # TODO: def test_invoice_getitems_created_at_from(self): + + # TODO:def test_invoice_getitems_created_at_to(self): + + # TODO: def test_invoice_getitems_updated_since(self): + + def test_invoice_getitems_query(self): + res = self.invoice_obj.create(customer_id=self.consumer.id) + sleep(3) + queryset = invoices.IuguInvoice.getitems(query=res.id) + self.assertEqual(queryset[0].customer_id, res.customer_id) + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # res.remove() + + def test_invoice_getitems_customer_id(self): + res = self.invoice_obj.create(customer_id=self.consumer.id) + sleep(3) + queryset = invoices.IuguInvoice.getitems(query=res.id) + self.assertEqual(queryset[0].customer_id, res.customer_id) + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # res.remove() + + @unittest.skip("API no support sort (in moment)") + def test_invoice_getitems_sort(self): + invoice_1 = self.invoice_obj.create() + invoice_2 = self.invoice_obj.create() + invoice_3 = self.invoice_obj.create() + sleep(3) + l1 = invoices.IuguInvoice.getitems(limit=3) + keep_checker = l1[2] + l2 = invoices.IuguInvoice.getitems(limit=3, sort="id") + skipped = l2[0] # after skip 2 the first must be keep_checker + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. + # invoice_1.remove() + # invoice_2.remove() + # invoice_3.remove() + self.assertEqual(keep_checker.id, skipped.id) + + +class TestPlans(unittest.TestCase): + + def setUp(self): + hash_md5 = md5() + seed = randint(1, 199) + variation = randint(4, 8) + hash_md5.update(str(seed)) + identifier = hash_md5.hexdigest()[:variation] + self.identifier = identifier # random because can't be repeated + plan = plans.IuguPlan() + self.plan = plan.create(name="My SetUp Plan", identifier=self.identifier, + interval=1, interval_type="months", + currency="BRL", value_cents=1500) + + # features + self.features = plans.Feature() + self.features.name = "Add feature %s" % self.identifier + self.features.identifier = self.identifier + self.features.value = 11 + + def tearDown(self): + self.plan.remove() + + def test_plan_create(self): + plan = plans.IuguPlan() + identifier = self.identifier + "salt" + new_plan = plan.create(name="My first lib Plan", identifier=identifier, + interval=1, interval_type="months", + currency="BRL", value_cents=1000) + self.assertIsInstance(new_plan, plans.IuguPlan) + self.assertTrue(new_plan.id) + new_plan.remove() + + def test_plan_create_without_required_fields(self): + plan = plans.IuguPlan() + self.assertRaises(errors.IuguPlansException, plan.create) + + def test_plan_create_features(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + # init object + plan = plans.IuguPlan(name="Plan with features", identifier=identifier, + interval=1, interval_type="months", + currency="BRL", value_cents=1000) + + plan.features = [self.features,] + new_plan_with_features = plan.create() + self.assertIsInstance(new_plan_with_features.features[0], plans.Feature) + self.assertEqual(new_plan_with_features.features[0].value, self.features.value) + new_plan_with_features.remove() + + def test_plan_get(self): + plan_id = self.plan.id + plan = plans.IuguPlan.get(plan_id) + self.assertEqual(self.identifier, plan.identifier) + + def test_plan_get_identifier(self): + plan = plans.IuguPlan.get_by_identifier(self.identifier) + self.assertEqual(self.identifier, plan.identifier) + + def test_plan_remove(self): + plan = plans.IuguPlan() + new_plan = plan.create(name="Remove me", identifier="to_remove", + interval=1, interval_type="months", + currency="BRL", value_cents=2000) + removed_id = new_plan.id + new_plan.remove() + self.assertRaises(errors.IuguGeneralException, + plans.IuguPlan.get, removed_id) + + def test_plan_edit_changes_name_by_set(self): + plan_id = self.plan.id + new_name = "New name %s" % self.identifier + modified_plan = self.plan.set(plan_id, name=new_name) + self.assertEqual(new_name, modified_plan.name) + + def test_plan_edit_changes_identifier_by_set(self): + plan_id = self.plan.id + new_identifier = "New identifier %s" % self.identifier + modified_plan = self.plan.set(plan_id, identifier=new_identifier) + self.assertEqual(new_identifier, modified_plan.identifier) + + def test_plan_edit_changes_interval_by_set(self): + plan_id = self.plan.id + new_interval = 3 + modified_plan = self.plan.set(plan_id, interval=new_interval) + self.assertEqual(new_interval, modified_plan.interval) + + def test_plan_edit_changes_currency_by_set(self): + plan_id = self.plan.id + new_currency = "US" + self.assertRaises(errors.IuguPlansException, self.plan.set, + plan_id, currency=new_currency) + + def test_plan_edit_changes_value_cents_by_set(self): + plan_id = self.plan.id + value_cents = 3000 + modified_plan = self.plan.set(plan_id, value_cents=value_cents) + self.assertEqual(value_cents, modified_plan.prices[0].value_cents) + + def test_plan_edit_changes_features_name_by_set(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features Name" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + changed_features = plan_returned.features + changed_features[0].name = "Changed Name of Features" + + # return plan changed + plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) + + self.assertEqual(plan_changed.features[0].name, + plan_returned.features[0].name) + plan_returned.remove() + + def test_plan_edit_changes_features_identifier_by_set(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features Identifier" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + changed_features = plan_returned.features + changed_features[0].identifier = "Crazy_Change" + + # return plan changed + plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) + + self.assertEqual(plan_changed.features[0].identifier, + plan_returned.features[0].identifier) + plan_returned.remove() + + def test_plan_edit_changes_features_value_by_set(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features Identifier" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + changed_features = plan_returned.features + changed_features[0].value = 10000 + + # return plan changed + plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) + + self.assertEqual(plan_changed.features[0].value, + plan_returned.features[0].value) + plan_returned.remove() + + def test_plan_edit_changes_name_by_save(self): + self.plan.name = "New name %s" % self.identifier + response = self.plan.save() + self.assertEqual(response.name, self.plan.name) + + def test_plan_edit_changes_identifier_by_save(self): + seed = randint(1, 999) + self.plan.identifier = "New_identifier_%s_%s" % (self.identifier, + seed) + response = self.plan.save() + self.assertEqual(response.identifier, self.plan.identifier) + + def test_plan_edit_changes_interval_by_save(self): + self.plan.interval = 4 + response = self.plan.save() + self.assertEqual(response.interval, 4) + + def test_plan_edit_changes_currency_by_save(self): + # API only support BRL + self.plan.currency = "US" + # response = self.plan.save() + self.assertRaises(errors.IuguPlansException, self.plan.save) + + def test_plan_edit_changes_value_cents_by_save(self): + self.plan.value_cents = 4000 + response = self.plan.save() + self.assertEqual(response.prices[0].value_cents, 4000) + + # TODO: test prices attribute of plan in level one + + def test_plan_edit_changes_features_name_by_save(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features by Save" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + to_change_features = plan_returned.features + to_change_features[0].name = "Features New by Save" + + # return plan changed and to save instance + plan_returned.features = [to_change_features[0]] + plan_changed = plan_returned.save() + + self.assertEqual(plan_changed.features[0].name, "Features New by Save") + plan_returned.remove() + + def test_plan_edit_changes_features_identifier_by_save(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features by Save" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + to_change_features = plan_returned.features + to_change_features[0].identifier = "Crazy_Changed" + + # return plan changed and to save instance + plan_returned.features = [to_change_features[0]] + plan_changed = plan_returned.save() + + self.assertEqual(plan_changed.features[0].identifier, "Crazy_Changed") + plan_returned.remove() + + def test_plan_edit_changes_features_value_by_save(self): + salt = randint(1, 99) + identifier = self.identifier + str(salt) + + # creating a plan with features + plan = plans.IuguPlan() + plan.features = [self.features,] + plan.name = "Changes Features by Save" + plan.identifier = identifier # workaround: setUp already creates + plan.interval = 2 + plan.interval_type = "weeks" + plan.currency = "BRL" + plan.value_cents = 3000 + plan_returned = plan.create() + + # to change features name where features already has an id + to_change_features = plan_returned.features + to_change_features[0].value = 8000 + + # return plan changed and to save instance + plan_returned.features = [to_change_features[0]] + plan_changed = plan_returned.save() + + self.assertEqual(plan_changed.features[0].value, 8000) + plan_returned.remove() + + def test_plan_getitems_filter_limit(self): + # creating a plan with features + salt = str(randint(1, 199)) + self.identifier + plan = plans.IuguPlan() + plan_a = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=1000) + salt = str(randint(1, 199)) + self.identifier + plan_b = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=2000) + salt = str(randint(1, 199)) + self.identifier + plan_c = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=3000) + + all_plans = plans.IuguPlan.getitems(limit=3) + self.assertEqual(len(all_plans), 3) + plan_a.remove() + plan_b.remove() + plan_c.remove() + + def test_plan_getitems_filter_skip(self): + # creating a plan with features + salt = str(randint(1, 199)) + self.identifier + plan = plans.IuguPlan() + plan_a = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=1000) + salt = str(randint(1, 199)) + self.identifier + plan_b = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=2000) + salt = str(randint(1, 199)) + self.identifier + plan_c = plan.create(name="Get Items...", + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=3000) + + sleep(2) + all_plans_limit = plans.IuguPlan.getitems(limit=3) + all_plans_skip = plans.IuguPlan.getitems(skip=2, limit=3) + self.assertEqual(all_plans_limit[2].id, all_plans_skip[0].id) + plan_a.remove() + plan_b.remove() + plan_c.remove() + + def test_plan_getitems_filter_query(self): + salt = str(randint(1, 199)) + self.identifier + name_repeated = salt + plan = plans.IuguPlan() + plan_a = plan.create(name=name_repeated, + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=1000) + salt = str(randint(1, 199)) + self.identifier + plan_b = plan.create(name=name_repeated, + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=2000) + salt = str(randint(1, 199)) + self.identifier + plan_c = plan.create(name=name_repeated, + identifier=salt, interval=2, + interval_type="weeks", currency="BRL", + value_cents=3000) + sleep(3) # waiting API to keep data + all_filter_query = plans.IuguPlan.getitems(query=name_repeated) + self.assertEqual(all_filter_query[0].name, name_repeated) + self.assertEqual(len(all_filter_query), 3) + plan_a.remove() + plan_b.remove() + plan_c.remove() + + #@unittest.skip("TODO support this test") + # TODO: def test_plan_getitems_filter_updated_since(self): + + #@unittest.skip("Sort not work fine. Waiting support of API providers") + #def test_plan_getitems_filter_sort(self): + + +class TestSubscriptions(unittest.TestCase): + + + def clean_invoices(self, recent_invoices): + """ + Removes invoices created in backgrounds of tests + """ + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. (API CHANGED) + # + #if recent_invoices: + # invoice = recent_invoices[0] + # invoices.IuguInvoice().remove(invoice_id=invoice["id"]) + pass + + def setUp(self): + # preparing object... + seed = randint(1, 10000) + md5_hash = md5() + md5_hash.update(str(seed)) + plan_id_random = md5_hash.hexdigest()[:12] + plan_name = "Subs Plan %s" % plan_id_random + name = "Ze %s" % plan_id_random + email = "{name}@example.com".format(name=plan_id_random) + # plans for multiple tests + self.plan_new = plans.IuguPlan().create(name=plan_name, + identifier=plan_id_random, + interval=1, + interval_type="weeks", + currency="BRL", + value_cents=9900) + + plan_identifier = "plan_for_changes_%s" % plan_id_random + self.plan_two = plans.IuguPlan().create(name="Plan Two", + identifier=plan_identifier, interval=1, + interval_type="weeks", currency="BRL", + value_cents=8800) + # one client + self.customer = customers.IuguCustomer().create(name=name, email=email) + # for tests to edit subscriptions + subs_obj = subscriptions.IuguSubscription() + self.subscription = subs_obj.create(customer_id=self.customer.id, + plan_identifier=self.plan_two.identifier) + + def tearDown(self): + # Attempt to delete the invoices created by subscriptions cases + # But this not remove all invoices due not recognizable behavior + # as the API no forever return recents_invoices for created + # invoices + + # The comments below was put because API don't exclude + # an invoice that was paid. So only does refund. (API CHANGED) + #### if self.subscription.recent_invoices: + #### invoice = self.subscription.recent_invoices[0] + #### # to instanciate invoice from list of the invoices returned by API + #### invoice_obj = invoices.IuguInvoice.get(invoice["id"]) + #### # The comments below was put because API don't exclude + #### # an invoice that was paid. So only does refund. + #### invoice_obj.remove() + self.plan_new.remove() + self.plan_two.remove() + self.subscription.remove() + self.customer.remove() + + def test_subscription_create(self): + # Test to create a subscription only client_id and plan_identifier + p_obj = subscriptions.IuguSubscription() + subscription_new = p_obj.create(self.customer.id, self.plan_new.identifier) + self.assertIsInstance(subscription_new, subscriptions.IuguSubscription) + self.assertEqual(subscription_new.plan_identifier, self.plan_new.identifier) + self.clean_invoices(subscription_new.recent_invoices) + subscription_new.remove() + + def test_subscription_create_with_custom_variables(self): + p_obj = subscriptions.IuguSubscription() + subscription_new = p_obj.create(self.customer.id, + self.plan_new.identifier, + custom_variables={'city':'Recife'}) + self.assertEqual(subscription_new.custom_variables[0]["name"], "city") + self.assertEqual(subscription_new.custom_variables[0]["value"], "Recife") + self.clean_invoices(subscription_new.recent_invoices) + subscription_new.remove() + + def test_subscription_set_with_custom_variables(self): + p_obj = subscriptions.IuguSubscription() + subscription_new = p_obj.set(sid=self.subscription.id, + custom_variables={'city':'Recife'}) + self.assertEqual(subscription_new.custom_variables[0]["name"], "city") + self.assertEqual(subscription_new.custom_variables[0]["value"], "Recife") + # self.clean_invoices(subscription_new.recent_invoices) + + @unittest.skip("API does not support this only_on_charge_success. CHANGED") + def test_subscription_create_only_on_charge_success_with_payment(self): + # Test to create subscriptions with charge only + customer = customers.IuguCustomer().create(name="Pay now", + email="pay_now@local.com") + pay = customer.payment.create(description="Payment X", + number="4111111111111111", + verification_value='123', + first_name="Romario", last_name="Baixo", + month=12, year=2018) + p_obj = subscriptions.IuguSubscription() + new_subscription = p_obj.create(customer.id, self.plan_new.identifier, + only_on_charge_success=True) + self.assertEqual(new_subscription.recent_invoices[0]["status"], "paid") + self.clean_invoices(new_subscription.recent_invoices) + new_subscription.remove() + customer.remove() + + def test_subscription_create_only_on_charge_success_less_payment(self): + # Test to create subscriptions with charge only + p_obj = subscriptions.IuguSubscription() + self.assertRaises(errors.IuguGeneralException, p_obj.create, + self.customer.id, self.plan_new.identifier, + only_on_charge_success=True) + + def test_subscription_remove(self): + # Test to remove subscription + p_obj = subscriptions.IuguSubscription() + subscription_new = p_obj.create(self.customer.id, self.plan_new.identifier) + sid = subscription_new.id + self.clean_invoices(subscription_new.recent_invoices) + subscription_new.remove() + self.assertRaises(errors.IuguGeneralException, + subscriptions.IuguSubscription.get, sid) + + def test_subscription_get(self): + subscription = subscriptions.IuguSubscription.get(self.subscription.id) + self.assertIsInstance(subscription, subscriptions.IuguSubscription) + + def test_subscription_getitems(self): + subscription_list = subscriptions.IuguSubscription.getitems() + self.assertIsInstance(subscription_list[0], subscriptions.IuguSubscription) + + def test_subscription_getitem_limit(self): + client_subscriptions = subscriptions.IuguSubscription() + sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sleep(3) # slower API + subscriptions_list = subscriptions.IuguSubscription.getitems(limit=1) + self.assertEqual(len(subscriptions_list), 1) + self.assertEqual(subscriptions_list[0].id, sub_4.id) + self.clean_invoices(sub_1.recent_invoices) + self.clean_invoices(sub_2.recent_invoices) + self.clean_invoices(sub_3.recent_invoices) + self.clean_invoices(sub_4.recent_invoices) + a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() + + def test_subscription_getitem_skip(self): + client_subscriptions = subscriptions.IuguSubscription() + sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sleep(2) + subscriptions_list = subscriptions.IuguSubscription.getitems(skip=1) + self.assertEqual(subscriptions_list[0].id, sub_3.id) + self.clean_invoices(sub_1.recent_invoices) + self.clean_invoices(sub_2.recent_invoices) + self.clean_invoices(sub_3.recent_invoices) + self.clean_invoices(sub_4.recent_invoices) + a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() + + # TODO: def test_subscription_getitem_created_at_from(self): + + def test_subscription_getitem_query(self): + term = self.customer.name + sleep(3) # very slow API! waiting... + subscriptions_list = subscriptions.IuguSubscription.getitems(query=term) + self.assertGreaterEqual(len(subscriptions_list), 1) + + # TODO: def test_subscription_getitem_updated_since(self): + + @unittest.skip("API not support this. No orders is changed") + def test_subscription_getitem_sort(self): + client_subscriptions = subscriptions.IuguSubscription() + sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + subscriptions_list = subscriptions.IuguSubscription.getitems(sort="-created_at") + #self.assertEqual(subscriptions_list[0].id, sub_3.id) + self.clean_invoices(sub_1.recent_invoices) + self.clean_invoices(sub_2.recent_invoices) + self.clean_invoices(sub_3.recent_invoices) + self.clean_invoices(sub_4.recent_invoices) + a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() + + def test_subscription_getitem_customer_id(self): + + client_subscriptions = subscriptions.IuguSubscription() + # previous subscription was created in setUp + sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) + sleep(3) + subscriptions_list = subscriptions.IuguSubscription.\ + getitems(customer_id=self.customer.id) + self.assertEqual(len(subscriptions_list), 3) # sub_1 + sub_2 + setUp + self.clean_invoices(sub_1.recent_invoices) + self.clean_invoices(sub_2.recent_invoices) + a, b = sub_1.remove(), sub_2.remove() + + def test_subscription_set_plan(self): + # Test to change an existent plan in subscription + subs = subscriptions.IuguSubscription() + subscription = subs.create(self.customer.id, self.plan_new.identifier) + sid = subscription.id + plan_identifier = self.plan_new.identifier + str("_Newest_ID") + # changes to this new plan + plan_newest = plans.IuguPlan().create("Plan Name: Newest", + plan_identifier, 1, "months", "BRL", 5000) + # editing... + subscription = subscriptions.IuguSubscription().set(sid, + plan_identifier=plan_newest.identifier) + self.assertEqual(subscription.plan_identifier, plan_identifier) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + plan_newest.remove() + + @unittest.skip("API does not support. It returns error 'Subscription Not Found'") + def test_subscription_set_customer_id(self): + # Test if customer_id changed. Iugu's support (number 782) + customer = customers.IuguCustomer().create(name="Cortella", + email="mcortella@usp.br") + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, customer_id=customer.id) + + self.assertEqual(subscription.customer_id, customer.id) + customer.remove() + + def test_subscription_set_expires_at(self): + # Test if expires_at was changed + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, expires_at="12/12/2014") + self.assertEqual(subscription.expires_at, "2014-12-12") + + def test_subscription_set_suspended(self): + # Test if suspended was changed + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, suspended=True) + self.assertEqual(subscription.suspended, True) + + @unittest.skip("Waiting API developers to support this question") + def test_subscription_set_skip_charge(self): + # Test if skip_charge was marked + print(self.subscription.id) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, skip_charge=True) + self.assertEqual(subscription.suspended, True) + + def test_subscription_set_subitems(self): + # Test if to insert a new item + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + self.assertEqual(subscription.subitems[0].description, + subitem.description) + + def test_subscription_set_subitems_description(self): + # Test if subitem/item descriptions was changed + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + item_with_id = subscription.subitems[0] + item_with_id.description = "Subitems Edited" + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[item_with_id,] ) + self.assertEqual(subscription.subitems[0].description, + item_with_id.description) + + def test_subscription_set_subitems_price_cents(self): + # Test if subitem/item price_cents was changed + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + item_with_id = subscription.subitems[0] + item_with_id.price_cents = 2900 + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[item_with_id,] ) + self.assertEqual(subscription.subitems[0].price_cents, + item_with_id.price_cents) + + def test_subscription_set_subitems_quantity(self): + # Test if subitem/item quantity was changed + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + item_with_id = subscription.subitems[0] + item_with_id.quantity = 4 + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[item_with_id,] ) + self.assertEqual(subscription.subitems[0].quantity, + item_with_id.quantity) + + def test_subscription_set_subitems_recurrent(self): + # Test if subitem/item recurrent was changed + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + item_with_id = subscription.subitems[0] + item_with_id.recurrent = True + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[item_with_id,]) + self.assertEqual(subscription.subitems[0].recurrent, + item_with_id.recurrent) + + def test_subscription_set_subitems_destroy(self): + # Test if subitem/item was erased + subitem = merchant.Item("Subitems", 1, 2345) + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[subitem,]) + item_with_id = subscription.subitems[0] + item_with_id.destroy = True + subscription = subscriptions.IuguSubscription().\ + set(self.subscription.id, subitems=[item_with_id,]) + self.assertEqual(subscription.subitems, []) + + def test_subscription_create_credit_based_with_custom_variables(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=10, + custom_variables={'city':"Recife"}) + self.assertEqual(subscription.custom_variables[0]['name'], "city") + self.assertEqual(subscription.custom_variables[0]['value'], "Recife") + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_set_credit_based_with_custom_variables(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=10) + subscription = subscriptions.SubscriptionCreditsBased().\ + set(subscription.id, custom_variables={'city':"Madrid"}) + self.assertEqual(subscription.custom_variables[0]['name'], "city") + self.assertEqual(subscription.custom_variables[0]['value'], "Madrid") + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_create_credit_based(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=10) + + self.assertIsInstance(subscription, subscriptions.SubscriptionCreditsBased) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_create_credit_based_error_price_cents(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased() + self.assertRaises(errors.IuguSubscriptionsException, + subscription.create, self.customer.id, + credits_cycle=2, price_cents=0) + + def test_subscription_create_credit_based_error_price_cents_empty(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased() + self.assertRaises(errors.IuguSubscriptionsException, + subscription.create, self.customer.id, + credits_cycle=2, price_cents=None) + + def test_subscription_create_credit_based_price_cents(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=2000) + + self.assertEqual(subscription.price_cents, 2000) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_create_credit_based_credits_cycle(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=2000) + + self.assertEqual(subscription.credits_cycle, 2) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_create_credit_based_credits_min(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=2000, + credits_min=4000) + + self.assertEqual(subscription.credits_min, 4000) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_set_credit_based_price_cents(self): + # Test if price_cents changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=1200) + + subscription = subscriptions.SubscriptionCreditsBased().\ + set(subscription.id, price_cents=3249) + + self.assertEqual(subscription.price_cents, 3249) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_set_credits_cycle(self): + # Test if credits_cycle changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=1300) + + subscription = subscriptions.SubscriptionCreditsBased().\ + set(subscription.id, credits_cycle=10) + + self.assertEqual(subscription.credits_cycle, 10) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_set_credits_min(self): + # Test if credits_min changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=1400) + + subscription = subscriptions.SubscriptionCreditsBased().\ + set(subscription.id, credits_min=2000) + + self.assertEqual(subscription.credits_min, 2000) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_credit_based_get(self): + # Test if credits_min changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=2000) + + subscription = subscriptions.SubscriptionCreditsBased().\ + get(subscription.id) + + self.assertIsInstance(subscription, subscriptions.SubscriptionCreditsBased) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_credit_based_getitems(self): + # Test if credits_min changed + subscription = subscriptions.SubscriptionCreditsBased().\ + create(self.customer.id, credits_cycle=2, price_cents=2000) + + sleep(2) + subscription_list = subscriptions.SubscriptionCreditsBased().\ + getitems() + + self.assertIsInstance(subscription_list[0], subscriptions.SubscriptionCreditsBased) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + # Test save method + + @unittest.skip("This is not support by API. Return not found") + def test_subscription_save_customer_id(self): + # Iugu's support (number 782) + customer = customers.IuguCustomer().create(name="Subs save", + email="subs_save@local.com") + self.subscription.customer_id = customer.id + obj = self.subscription.save() + self.assertEqual(customer.id, obj.customer_id) + customer.remove() + + def test_subscription_save_expires_at(self): + self.subscription.expires_at = "12/12/2020" + obj = self.subscription.save() + self.assertEqual(obj.expires_at, "2020-12-12") + + def test_subscription_save_subitems(self): + # Test if to save a new item + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + obj = self.subscription.save() + self.assertEqual(obj.subitems[0].description, + subitem.description) + + def test_subscription_save_subitems_description(self): + # Test if subitem/item descriptions was changed + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + new_subscription = self.subscription.save() + item_with_id = new_subscription.subitems[0] + item_with_id.description = "Subitems Edited" + self.subscription.subitems = [item_with_id] + obj = self.subscription.save() + self.assertEqual(obj.subitems[0].description, + item_with_id.description) + + def test_subscription_save_subitems_price_cents(self): + # Test if subitem/item price_cents was changed + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + new_subscription = self.subscription.save() + item_with_id = new_subscription.subitems[0] + item_with_id.price_cents = 2900 + self.subscription.subitems = [item_with_id,] + obj = self.subscription.save() + self.assertEqual(obj.subitems[0].price_cents, + item_with_id.price_cents) + + def test_subscription_save_subitems_quantity(self): + # Test if subitem/item quantity was changed + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + new_subscription = self.subscription.save() + item_with_id = new_subscription.subitems[0] + item_with_id.quantity = 4 + self.subscription.subitems = [item_with_id,] + obj = self.subscription.save() + self.assertEqual(obj.subitems[0].quantity, + item_with_id.quantity) + + def test_subscription_save_subitems_recurrent(self): + # Test if subitem/item recurrent was changed + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + new_subscription = self.subscription.save() + item_with_id = new_subscription.subitems[0] + item_with_id.recurrent = True + self.subscription.subitems = [item_with_id,] + obj = self.subscription.save() + self.assertEqual(obj.subitems[0].recurrent, + item_with_id.recurrent) + + def test_subscription_save_subitems__destroy(self): + # Test if subitem/item was erased + subitem = merchant.Item("Subitems", 1, 2345) + self.subscription.subitems = [subitem,] + new_subscription = self.subscription.save() + item_with_id = new_subscription.subitems[0] + item_with_id.destroy = True + self.subscription.subitems = [item_with_id,] + obj = self.subscription.save() + self.assertEqual(obj.subitems, []) + + def test_subscription_save_suspended(self): + self.subscription.suspended = True + obj = self.subscription.save() + self.assertEqual(obj.suspended, True) + + # @unittest.skip("Waiting API developers to support this question") + # TODO: def test_subscription_save_skip_charge(self): + + def test_subscription_save_price_cents(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1000) + subscription.price_cents = 8188 + obj = subscription.save() + self.assertEqual(obj.price_cents, 8188) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_save_credits_cycle(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1000) + subscription.credits_cycle = 5 + obj = subscription.save() + self.assertEqual(obj.credits_cycle, 5) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_save_credits_min(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1100) + subscription.credits_min = 9000 + obj = subscription.save() + self.assertEqual(obj.credits_min, 9000) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_suspend(self): + obj = subscriptions.IuguSubscription().suspend(self.subscription.id) + self.assertEqual(obj.suspended, True) + + @unittest.skip("API not support this activate by REST .../activate") + def test_subscription_activate(self): + obj = subscriptions.IuguSubscription().suspend(self.subscription.id) + self.subscription.suspended = True + self.subscription.save() + obj = subscriptions.IuguSubscription().activate(self.subscription.id) + self.assertEqual(obj.suspended, False) + + def test_subscription_change_plan(self): + seed = randint(1, 999) + identifier = "%s_%s" % (self.plan_new.identifier, str(seed)) + plan_again_change = plans.IuguPlan().create(name="Change Test", + identifier=identifier, + interval=1, interval_type="months", + currency="BRL", value_cents=1111) + obj = subscriptions.IuguSubscription().change_plan( + plan_again_change.identifier, + sid=self.subscription.id) + self.assertEqual(obj.plan_identifier, identifier) + self.clean_invoices(obj.recent_invoices) + plan_again_change.remove() + + def test_subscription_change_plan_by_instance(self): + seed = randint(1, 999) + identifier = "%s_%s" % (self.plan_new.identifier, str(seed)) + plan_again_change = plans.IuguPlan().create(name="Change Test", + identifier=identifier, + interval=1, interval_type="months", + currency="BRL", value_cents=1112) + obj = self.subscription.change_plan(plan_again_change.identifier) + self.assertEqual(obj.plan_identifier, identifier) + self.clean_invoices(obj.recent_invoices) + plan_again_change.remove() + + def test_subscription_add_credits(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1100) + obj = subscriptions.SubscriptionCreditsBased().add_credits(sid=subscription.id, + quantity=20) + self.assertEqual(obj.credits, 20) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_add_credits_by_instance(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1100) + obj = subscription.add_credits(sid=subscription.id, + quantity=20) + self.assertEqual(obj.credits, 20) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_remove_credits(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1100) + + subscription.add_credits(quantity=20) + obj = subscriptions.SubscriptionCreditsBased().\ + remove_credits(sid=subscription.id, quantity=5) + self.assertEqual(obj.credits, 15) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + def test_subscription_remove_credits_by_instance(self): + subscription = subscriptions.SubscriptionCreditsBased() + subscription = subscription.create(customer_id=self.customer.id, + credits_cycle=2, price_cents=1100) + + subscription.add_credits(quantity=20) + sleep(2) + obj = subscription.remove_credits(quantity=5) + self.assertEqual(obj.credits, 15) + self.clean_invoices(subscription.recent_invoices) + subscription.remove() + + +class TestTransfer(unittest.TestCase): + # TODO: to create this tests + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/lib/iugu/utils.py b/lib/iugu/utils.py new file mode 100644 index 0000000..355ff0f --- /dev/null +++ b/lib/iugu/utils.py @@ -0,0 +1,78 @@ +""" +Useful for developers purposes +""" + +from . import config, subscriptions, customers, invoices, plans + +if config.API_MODE_TEST is not True: + raise TypeError("::DANGER:: You isn't in mode test. Check your config file") + +def loop_remove(objs_list, verbose=0): + """ Removes all objects of a list returned by getitems() + """ + for i in objs_list: + try: + i.remove() + except: + if verbose >= 2: + print("Not removed due an exception: ", i.id) + else: + pass + +def try_remove(client, verbose=0): + objs = client.getitems() + loop_remove(objs, verbose=verbose) + objs = client.getitems() + return len(objs) + +def try_remove_subs(verbose=0): + subs = subscriptions.IuguSubscription + total = try_remove(subs, verbose=verbose) + if verbose >= 1: + print("Total of Subscriptions: ", total) + return total + +def try_remove_client(verbose=0): + cvs = customers.IuguCustomer + total = try_remove(cvs, verbose=verbose) + if verbose >= 1: + print("Total of Customers: ", total) + return total + +def try_remove_invoices(verbose=0): + ivs = invoices.IuguInvoice + total = try_remove(ivs, verbose=verbose) + if verbose >= 1: + print("Total of Invoices: ", total) + return total + +def try_remove_plans(verbose=0): + pls = plans.IuguPlan + total = try_remove(pls, verbose=verbose) + if verbose >= 1: + print("Total of Plans: ", total) + return total + +def reset_all(tolerance=100, verbose=1): + """ + Tries to remove all data + + :param tolerance: it's the number of items not deleted because of API's errors + + IMPORTANT: a tolerance very little (e.g. 10) can to cause a loop + """ + # TODO function ... + tolerance_limit = tolerance + (tolerance * .20) + operations_to_remove = [try_remove_plans, try_remove_subs, + try_remove_client, try_remove_invoices] + def _loop_(operation): + r = tolerance * 2 + while r > tolerance: + r = operation(verbose=verbose) + if r > tolerance_limit: + break + + for op in operations_to_remove: + _loop_(op) + + diff --git a/lib/iugu/version.py b/lib/iugu/version.py new file mode 100644 index 0000000..6c49ec1 --- /dev/null +++ b/lib/iugu/version.py @@ -0,0 +1,8 @@ +__version__ = "0.9.6" + +#------------------------------------------------------------------------------ +# CHANGELOG: +# ... oldest changes +# 2014-07-07 v0.9.3 - Removed requirement of config.py variables (token, account) +# 2015-02-22 v0.9.4 - Supported changes from API +# 2015-02-22 v0.9.5-1+ - Supported changes from API diff --git a/requirement-dev.txt b/requirement-dev.txt new file mode 100644 index 0000000..7eaadce --- /dev/null +++ b/requirement-dev.txt @@ -0,0 +1,5 @@ +coverage==3.7.1 +nose==1.3.3 +python-dateutil==2.2 +six==1.6.1 +wsgiref==0.1.2 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4069969 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +try: + from distutils.core import setup +except ImportError: + from setuptools import setup + +import sys, os +from setuptools import find_packages +# sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib')) +# from iugu.version import __version__ + +setup( + name='iugu-python3', + version='0.1', + author='Fellipe Henrique', + author_email='fellipeh@gmail.com', + packages=find_packages('lib'), + package_dir={'': 'lib'}, + scripts=[], + url='https://github.com/fellipeh/iugu-python3', + download_url='https://github.com/fellipeh/iugu-python3/tarball/master', + license='Apache License', + description='This package is an idiomatic python lib to work with Iugu service', + long_description=""" + This iugu-python3 lib is the more pythonic way to work with webservices of payments + iugu.com. This provides python objects to each entity of the service as Subscriptions, + Plans, Customers, Invoices, etc. + http://iugu.com/referencias/api - API Reference +""", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + keywords=['iugu', 'rest', 'payment'] +)