├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── lib ├── __init__.py └── iugu │ ├── __init__.py │ ├── base.py │ ├── config.py │ ├── customers.py │ ├── errors.py │ ├── invoices.py │ ├── merchant.py │ ├── plans.py │ ├── subscriptions.py │ ├── tests.py │ ├── utils.py │ └── version.py ├── requirement-dev.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | settings_dev.py 3 | .pydevproject 4 | .project 5 | *.bak 6 | lib/iugu/config.py 7 | bin 8 | build 9 | include 10 | local 11 | man 12 | share 13 | *.pyc 14 | *~ 15 | *.db 16 | .DS_Store 17 | .idea 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Iugu python API 2 | ============================================ 3 | 4 | This package is the more idiomatic python lib to work with Iugu service. The lib is compatible with 5 | Iugu API v1 6 | 7 | Overview 8 | -------- 9 | 10 | 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 11 | 12 | Prerequisites 13 | ------------- 14 | In order to use the code in this package, you need to obtain an account 15 | (API key) from http://iugu.com/referencias/api. You'll also find full API 16 | documentation on that page. 17 | 18 | In order to run the sample code, you need a user account on the test mode 19 | service where you will do your development. Sign up for an account at 20 | https://iugu.com/signup and change mode in https://iugu.com/a/administration 21 | ("Modo de Teste") 22 | 23 | In order to run the client sample code, you need a account user token. This is 24 | automatically created. See https://iugu.com/settings/profile 25 | 26 | Quick Install 27 | ----- 28 | ### Using pip ### 29 | ``` 30 | pip install iugu-python 31 | ``` 32 | or 33 | ### Using setup.py ### 34 | ``` 35 | # Downloading package master or release: 36 | # https://github.com/horacioibrahim/iugu-python/archive/master.zip 37 | # or https://github.com/horacioibrahim/iugu-python/releases 38 | unzip iugu-python-master.zip 39 | cd iugu-python-master 40 | python setup.py install 41 | ``` 42 | 43 | Usage (Quick Start) 44 | ----- 45 | ### Export environment variable IUGU_API_TOKEN ### 46 | ``` 47 | # For linux users: 48 | export IUGU_API_TOKEN=XXX 49 | ``` 50 | ### Merchant operations ### 51 | ``` 52 | from iugu.merchant import IuguMerchant, Item 53 | client = IuguMerchant(account_id="YOUR ACCOUN ID", 54 | api_mode_test=True) 55 | token = client.create_payment_token('4111111111111111', 'JA', 'Silva', 56 | '12', '2010', '123') 57 | => https://api.iugu.com/v1/payment_token 58 | ``` 59 | How create an item for charge 60 | ``` 61 | item = Item("Produto My Test", 1, 10000) 62 | ``` 63 | Now you can to create charge 64 | ```python 65 | charge = client.create_charge(EMAIL_CUSTOMER, item, token=token.id) 66 | ``` 67 | Check invoice ID created. All payment pass by Invoice. 68 | ``` 69 | charge.invoice_id 70 | ``` 71 | Or create a blank_slip ("Boleto Bancário") 72 | ``` 73 | charge = client.create_charge(EMAIL_CUSTOMER, item) 74 | => https://api.iugu.com/v1/charge 75 | ``` 76 | ### Customer operations ### 77 | ```python 78 | from iugu.customers import IuguCustomer 79 | client = IuguCustomer() 80 | customer = client.create(email='your_customer@example.com') 81 | => https://api.iugu.com/v1/customers 82 | ``` 83 | Now you can to retrieve customer 84 | ``` 85 | client.get(customer_id) 86 | ``` 87 | You can edit existent customer 88 | ``` 89 | client.set(CUSTOMER_ID, name="Sub Zero Wins") 90 | ``` 91 | Or you can to use save() 92 | ``` 93 | customer.name = "Sub Zero Wins" 94 | customer.save() 95 | ``` 96 | To remove or delete customer 97 | ``` 98 | client.delete(CONSUMER_ID) # by id 99 | or 100 | customer.remove() # by current instance 101 | ``` 102 | ### Operations with lists of customer ### 103 | Get all customer 104 | ```python 105 | from iugu.customers import IuguCustomer 106 | client = IuguCustomer() 107 | # your flavor of options 108 | # client.getitems([limit, skip, query, sort, created_at_from, created_at_to, 109 | # updated_since]) 110 | 111 | Use one option per time. Samples: 112 | client.getitems(limit=30) # Get most recent (latest) 30 customers 113 | client.getitems(skip=14) # Skip X customers. Useful for pagination 114 | client.getitems(updated_since="2014-06-05T15:02:40-03:00") 115 | 116 | In tests SORT is not support by API: 117 | client.getitems(sort="-name") # Sort by field >>name<< (descending) 118 | client.getitems(sort="name") # Sort by field >>name<< (ascending) 119 | 120 | => http://iugu.com/referencias/api#listar-os-clientes 121 | ``` 122 | ### Operations with Invoices ### 123 | Create an invoice 124 | ``` 125 | from iugu.invoices import IuguInvoice 126 | from iugu.merchant import Item 127 | 128 | item = Item("Curso: High Self Learning", 1, 6900) # qtd:1; price: 69,00 129 | invoice_obj = IuguInvoice() 130 | new_invoice = invoice_obj.create(due_date='24/06/2014', 131 | email='customer@example.com', items=item) 132 | ``` 133 | Get invoice by id 134 | ``` 135 | # not is need previous instance/obj (classmethod) 136 | invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') 137 | ``` 138 | Get all invoices 139 | ``` 140 | # not is need previous instance/obj 141 | invoices = IuguInvoice.getitems() # outcomes list of invoices (max 100 by API) 142 | ``` 143 | Get all with filter 144 | ``` 145 | invoices = IuguInvoice.getitems(limit=10) 146 | invoices = IuguInvoice.getitems(skp=5) 147 | invoices = IuguInvoice.getitems(sort="-email") # DESC 148 | invoices = IuguInvoice.getitems(sort="email") # ASC 149 | ... 150 | ``` 151 | Edit/change invoice. Only invoices with status "draft" can be changed all fields 152 | otherwise (if status pending, cancel or paid) only the logs field can to change 153 | ``` 154 | invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') 155 | invoice_existent.email = "other@other.com" 156 | invoice_existent.save() 157 | ``` 158 | Remove 159 | ``` 160 | invoice_existent.remove() 161 | ``` 162 | Cancel 163 | ``` 164 | IuguInvoice.to_cancel('A4AF853BC5714380A8708B2A4EDA27B3') 165 | ``` 166 | Refund 167 | ``` 168 | invoice_existent = IuguInvoice.get('A4AF853BC5714380A8708B2A4EDA27B3') 169 | invoice_existent.refund() 170 | ``` 171 | ### Operations with Subscriptions ### 172 | Create a subscription 173 | ``` 174 | from subscriptions import IuguSubscription 175 | client = IuguSubscription() 176 | # from plans import IuguPan 177 | # plan_x = IuguPlan().create("Plano Collor", "plano_collor") 178 | # from customers import IuguCustomer 179 | # mario = IuguCustomer().create(email='supermario@gmail.com') 180 | # subscription = client.create(customer_id=mario.id, 181 | # plan_identifier=plan_x.identifier) 182 | subscription = client.create(customer_id="XXX", plan_identifier="XXX") 183 | ``` 184 | Get one 185 | ``` 186 | subscription = IuguSubscription.get('ID') 187 | ``` 188 | Edit/Change 189 | ``` 190 | subscription = IuguSubscription.get('ID') 191 | subscription.expires_at = "14/07/2014" 192 | subscription.save() 193 | ``` 194 | Remove 195 | ``` 196 | subscription.remove() 197 | ``` 198 | 199 | ### References ### 200 | - API Document: http://iugu.com/referencias/api 201 | 202 | 203 | Known Issues 204 | ------------ 205 | ### Date Types ### 206 | It's need to use date formatted as string "2014-06-05T15:02:40-03:00", 207 | but in new release date will python date. 208 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horacioibrahim/iugu-python/5fd174a6ad7e77f27b72c4b05bd084c2a8a36cbe/lib/__init__.py -------------------------------------------------------------------------------- /lib/iugu/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2009-2014 hipy.co, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Iugu client for Iugu API.""" 16 | -------------------------------------------------------------------------------- /lib/iugu/base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | __author__ = 'horacioibrahim' 3 | 4 | import os 5 | from httplib import HTTPSConnection, CannotSendRequest, BadStatusLine 6 | from urllib import urlencode 7 | from json import load as json_load 8 | 9 | # python-iugu package modules 10 | import errors, config 11 | 12 | class IuguApi(object): 13 | 14 | """ 15 | Contains basics info to requests with API 16 | 17 | account_id: 18 | api_user: 19 | api_mode_test: 20 | """ 21 | try: 22 | API_TOKEN = os.environ["IUGU_API_TOKEN"] #config.API_TOKEN 23 | except KeyError: 24 | raise errors.IuguConfigException("Required environment variable " \ 25 | "IUGU_API_TOKEN") 26 | 27 | def __init__(self, **options): 28 | self.account_id = options.get('account_id') 29 | self.api_user = options.get('api_user') 30 | self.api_mode_test = options.get('api_mode_test') # useful for payment_token 31 | 32 | def is_debug(self): 33 | """Returns debug mode in config""" 34 | return config.DEBUG 35 | 36 | def is_mode_test(self): 37 | """Check if api_mode_test is True or False. 38 | Return string of the boolean 39 | """ 40 | 41 | mode = "true" if self.api_mode_test is True else "false" 42 | return mode 43 | 44 | def custom_variables_list(self, custom_variables): 45 | """Unpacking dictionary of keywords arguments and returns a list with 46 | data fit for to send API""" 47 | 48 | custom_data = [] # used to extend custom_variables in data_set() 49 | if isinstance(custom_variables, dict): 50 | # TODO: list comprehensions 51 | for k, v in custom_variables.items(): 52 | custom_data.append(("custom_variables[][name]", k.lower())) 53 | custom_data.append(("custom_variables[][value]", v)) 54 | 55 | if custom_data: 56 | return custom_data 57 | 58 | return None 59 | 60 | 61 | class IuguRequests(IuguApi): 62 | 63 | """ 64 | All request to API pass by here. Use the HTTP verbs for each request. For 65 | each method (get, post, put and delete) is need an URN and a list of fields 66 | its passed as list of tuples that is encoded by urlencode (e.g: 67 | [("field_api", "value")] 68 | 69 | URN: is relative path of URL http://api.iugu.com/ARG1/ARG2 where 70 | URN = "/ARG1/ARG2" 71 | 72 | All methods appends an api_token that is encoded in url params. The 73 | api_token is given in config.py its useful to work in sandbox mode. 74 | 75 | :method get: make a GET request 76 | :method post: make a POST request 77 | :method put: make a PUT request 78 | :method delete: make a DELETE request 79 | """ 80 | 81 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 82 | __conn = HTTPSConnection(config.API_HOSTNAME) # not put in instance 83 | __conn.timeout = 10 84 | 85 | def __init__(self, **options): 86 | super(IuguRequests, self).__init__(**options) 87 | 88 | if self.is_debug(): 89 | # set debuglevel to HTTPSConnection 90 | self.__conn.set_debuglevel(2) 91 | 92 | def __validation(self, response, msg=None): 93 | """ 94 | Validates if data returned by API contains errors json. The API returns 95 | by default a json with errors as field {errors: XXX} 96 | 97 | => http://iugu.com/referencias/api#erros 98 | """ 99 | results = json_load(response) 100 | 101 | try: 102 | err = results['errors'] 103 | except: 104 | err = None 105 | 106 | if err: 107 | raise errors.IuguGeneralException(value=err) 108 | else: 109 | return results 110 | 111 | def __reload_conn(self): 112 | """ 113 | Wrapper to keep TCP connection ESTABLISHED. Rather the connection go to 114 | CLOSE_WAIT and raise errors CannotSendRequest or the server reply with 115 | empty and it raise BadStatusLine 116 | """ 117 | self.__conn = HTTPSConnection(config.API_HOSTNAME) # reload 118 | self.__conn.timeout = 10 119 | 120 | def __conn_request(self, http_verb, urn, params): 121 | """ 122 | Wrapper to request/response of httplib's context, reload a 123 | connection if presume that error will occurs and returns the response 124 | """ 125 | try: 126 | self.__conn.request(http_verb, urn, params, self.headers) 127 | except CannotSendRequest: 128 | self.__reload_conn() 129 | self.__conn.request(http_verb, urn, params, self.headers) 130 | 131 | try: 132 | response = self.__conn.getresponse() 133 | except (IOError, BadStatusLine): 134 | self.__reload_conn() 135 | self.__conn.request(http_verb, urn, params, self.headers) 136 | response = self.__conn.getresponse() 137 | 138 | return response 139 | 140 | def get(self, urn, fields): 141 | fields.append(("api_token", self.API_TOKEN)) 142 | params = urlencode(fields, True) 143 | response = self.__conn_request("GET", urn, params) 144 | return self.__validation(response) 145 | 146 | def post(self, urn, fields): 147 | fields.append(("api_token", self.API_TOKEN)) 148 | params = urlencode(fields, True) 149 | response = self.__conn_request("POST", urn, params) 150 | return self.__validation(response) 151 | 152 | def put(self, urn, fields): 153 | fields.append(("api_token", self.API_TOKEN)) 154 | params = urlencode(fields, True) 155 | response = self.__conn_request("PUT", urn, params) 156 | return self.__validation(response) 157 | 158 | def delete(self, urn, fields): 159 | fields.append(("api_token", self.API_TOKEN)) 160 | params = urlencode(fields, True) 161 | response = self.__conn_request("DELETE", urn, params) 162 | return self.__validation(response) 163 | -------------------------------------------------------------------------------- /lib/iugu/config.py: -------------------------------------------------------------------------------- 1 | __author__ = 'horacioibrahim' 2 | 3 | 4 | METHOD_PAYMENT_CREDIT_CARD = "credit_card" 5 | 6 | DEBUG = False # Print logs/messages in console. 7 | 8 | 9 | # No share data 10 | API_HOSTNAME = "api.iugu.com" 11 | -------------------------------------------------------------------------------- /lib/iugu/customers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = 'horacioibrahim' 4 | 5 | # python-iugu package modules 6 | import base, config, errors 7 | 8 | class IuguCustomer(base.IuguApi): 9 | 10 | __conn = base.IuguRequests() 11 | 12 | def __init__(self, **options): 13 | """ 14 | This class is a CRUD for customers in API 15 | 16 | :param **options: receives dictionary load by JSON with fields of API 17 | 18 | => http://iugu.com/referencias/api#clientes 19 | """ 20 | super(IuguCustomer, self).__init__(**options) 21 | self.id = options.get("id") 22 | self.email = options.get("email") 23 | self.default_payment_method_id = options.get("default_payment_method_id") 24 | self.name = options.get("name") 25 | self.notes = options.get("notes") 26 | # TODO: convert str date in date type 27 | # year, month, day = map(int, string_date.split('-')) 28 | # date_converted = Date(day, month, year) 29 | self.created_at = options.get("created_at") 30 | # TODO: convert str date in date type 31 | self.updated_at = options.get("updated_at") 32 | self.custom_variables = options.get("custom_variables") 33 | self.payment = IuguPaymentMethod(self) 34 | 35 | def create(self, name=None, notes=None, email=None, custom_variables=None): 36 | """Creates a customer and return an IuguCustomer's instance 37 | 38 | :param name: customer name 39 | :param notes: field to post info's of an user 40 | :param email: required data of an user 41 | :param custom_variables: a dict {'key':'value'} 42 | """ 43 | data = [] 44 | urn = "/v1/customers" 45 | 46 | if name: 47 | data.append(("name", name)) 48 | 49 | if notes: 50 | data.append(("notes", notes)) 51 | 52 | if email: 53 | self.email = email 54 | 55 | if self.email: 56 | data.append(("email", self.email)) 57 | else: 58 | raise errors.IuguGeneralException(value="E-mail required is empty") 59 | 60 | if custom_variables: 61 | custom_data = self.custom_variables_list(custom_variables) 62 | data.extend(custom_data) 63 | customer = self.__conn.post(urn, data) 64 | instance = IuguCustomer(**customer) 65 | 66 | return instance 67 | 68 | def set(self, customer_id, name=None, notes=None, custom_variables=None): 69 | """ Updates/changes a customer that already exists 70 | 71 | :param custom_variables: is a dict {'key':'value'} 72 | HINT: Use method save() at handling an instance 73 | """ 74 | data = [] 75 | urn = "/v1/customers/{customer_id}".format(customer_id=str(customer_id)) 76 | 77 | if name: 78 | data.append(("name", name)) 79 | 80 | if notes: 81 | data.append(("notes", notes)) 82 | 83 | if custom_variables: 84 | custom_data = self.custom_variables_list(custom_variables) 85 | data.extend(custom_data) 86 | 87 | customer = self.__conn.put(urn, data) 88 | 89 | return IuguCustomer(**customer) 90 | 91 | def save(self): 92 | """Save updating a customer's instance""" 93 | return self.set(self.id, name=self.name, notes=self.notes) 94 | 95 | @classmethod 96 | def get(self, customer_id): 97 | """Gets one customer based in iD and returns an instance""" 98 | data = [] 99 | urn = "/v1/customers/{customer_id}".format(customer_id=str(customer_id)) 100 | customer = self.__conn.get(urn, data) 101 | instance = IuguCustomer(**customer) 102 | 103 | return instance 104 | 105 | @classmethod 106 | def getitems(self, limit=None, skip=None, created_at_from=None, 107 | created_at_to=None, query=None, updated_since=None, sort=None): 108 | """ 109 | Get a list of customers and return a list of IuguCustomer's instances. 110 | 111 | :param limit: limits the number of customers returned by API (default 112 | and immutable of API is 100) 113 | :param skip: skips a numbers of customers where more recent insert 114 | ordering. Useful to pagination 115 | :param query: filters based in value (case insensitive) 116 | :param sort: sorts based in field. Use minus signal to determine the 117 | direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API 118 | :return: list of IuguCustomer instances 119 | """ 120 | data = [] 121 | urn = "/v1/customers/" 122 | 123 | # Set options 124 | if limit: 125 | data.append(("limit", limit)) 126 | if skip: 127 | data.append(("start", skip)) 128 | if created_at_from: 129 | data.append(("created_at_from", created_at_from)) 130 | if created_at_to: 131 | data.append(("created_at_to", created_at_to)) 132 | if updated_since: 133 | data.append(("updated_since", updated_since)) 134 | if query: 135 | data.append(("query", query)) 136 | 137 | # TODO: sort not work fine. Waiting support of API providers 138 | if sort: 139 | assert sort is not str, "sort must be string as -name or name" 140 | 141 | if sort.startswith("-"): 142 | sort = sort[1:] 143 | key = "sortBy[{field}]".format(field=sort) 144 | data.append((key, "desc")) 145 | else: 146 | key = "sortBy[{field}]".format(field=sort) 147 | data.append((key, "asc")) 148 | 149 | customers = self.__conn.get(urn, data) 150 | 151 | #TODO: list comprehensions 152 | customers_objects = [] 153 | for customer in customers["items"]: 154 | obj_customer = IuguCustomer(**customer) 155 | customers_objects.append(obj_customer) 156 | 157 | return customers_objects 158 | 159 | def delete(self, customer_id=None): 160 | """Deletes a customer of instance or by id. 161 | And return the removed object""" 162 | data = [] 163 | 164 | if self.id: 165 | # instance of class (customer already exist) 166 | _customer_id = self.id 167 | else: 168 | if customer_id: 169 | _customer_id = customer_id 170 | else: 171 | # instance of class (not saved) 172 | raise TypeError("It's not instance of object returned or " \ 173 | "customer_id is empty.") 174 | 175 | urn = "/v1/customers/" + str(_customer_id) 176 | customer = self.__conn.delete(urn, data) 177 | 178 | return IuguCustomer(**customer) 179 | 180 | remove = delete # remove for keep the semantic of API 181 | 182 | 183 | class IuguPaymentMethod(object): 184 | 185 | """ 186 | A customer have multiple payments methods with only one default. This class 187 | allows handling Payment Methods. 188 | 189 | => http://iugu.com/referencias/api#formas-de-pagamento-de-cliente 190 | """ 191 | 192 | def __init__(self, customer, item_type="credit_card", **kwargs): 193 | assert isinstance(customer, IuguCustomer), "Customer invalid." 194 | _data = kwargs.get('data') 195 | self.customer_id = kwargs.get('customer_id') # useful create Payment by customer ID 196 | self.customer = customer 197 | self.description = kwargs.get('description') 198 | self.item_type = item_type # support only credit_card 199 | if _data: 200 | self.token = _data.get('token') # data credit card token 201 | self.display_number = _data.get('display_number') 202 | self.brand = _data.get('brand') 203 | self.holder_name = _data.get('holder_name') 204 | # self.set_as_default = kwargs.get('set_as_default') 205 | self.id = kwargs.get('id') 206 | 207 | # constructor payment 208 | data = kwargs.get('data') 209 | if data and isinstance(data, dict): 210 | self.payment_data = PaymentTypeCreditCard(**data) 211 | else: 212 | self.payment_data = PaymentTypeCreditCard() 213 | 214 | self.__conn = base.IuguRequests() 215 | 216 | def create(self, customer_id=None, description=None, number=None, 217 | verification_value=None, first_name=None, last_name=None, 218 | month=None, year=None, token=None, set_as_default=False): 219 | """ Creates a payment method for a customer and returns the own class 220 | 221 | :param customer_id: id of customer. You can pass in init or here 222 | :param description: required to create method. You can pass in init 223 | or here 224 | 225 | IMPORTANT: The API assert that data is optional, but is not real 226 | behavior. The values as number, verification_value, first_name, last_name, 227 | month and year are required args. 228 | """ 229 | data = [] 230 | 231 | # check if customer_id 232 | if customer_id: 233 | self.customer_id = customer_id 234 | else: 235 | self.customer_id = self.customer.id 236 | 237 | if description: 238 | self.description = description 239 | 240 | # we can create description when to instance or here (in create) 241 | assert self.description is not None, "description is required" 242 | 243 | if self.customer_id: 244 | urn = "/v1/customers/{customer_id}/payment_methods" \ 245 | .format(customer_id=str(self.customer.id)) 246 | else: 247 | raise errors.IuguPaymentMethodException 248 | 249 | # mounting data... 250 | data.append(("description", self.description)) 251 | data.append(("set_as_default", set_as_default)) 252 | 253 | if token: 254 | # if has token, card data it isn't need. 255 | self.token = token 256 | data.append(("token", self.token )) 257 | else: 258 | data.append(("item_type", self.item_type )) 259 | if number: 260 | self.payment_data.number = number 261 | 262 | if verification_value: 263 | self.payment_data.verification_value = verification_value 264 | 265 | if first_name: 266 | self.payment_data.first_name = first_name 267 | 268 | if last_name: 269 | self.payment_data.last_name = last_name 270 | 271 | if month: 272 | self.payment_data.month = month 273 | 274 | if year: 275 | self.payment_data.year = year 276 | 277 | if self.payment_data.is_valid(): 278 | # It's possible create payment method without credit card data. 279 | # Therefore this check is need. 280 | payment = self.payment_data.to_data() 281 | data.extend(payment) 282 | 283 | response = self.__conn.post(urn, data) 284 | 285 | return IuguPaymentMethod(self.customer, **response) 286 | 287 | def get(self, payment_id, customer_id=None): 288 | """ Returns a payment method of an user with base payment ID""" 289 | data = [] 290 | payment_id = str(payment_id) 291 | 292 | if customer_id is None: 293 | if self.customer.id: 294 | customer_id = self.customer.id 295 | else: 296 | raise TypeError("Customer or customer_id is not be None") 297 | 298 | urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ 299 | format(customer_id=customer_id, payment_id=payment_id) 300 | response = self.__conn.get(urn, data) 301 | 302 | return IuguPaymentMethod(self.customer, **response) 303 | 304 | def getitems(self, customer_id=None): 305 | """ 306 | Gets payment methods of a customer and returns a list of payment's 307 | methods instances (API limit is 100) 308 | """ 309 | data = [] 310 | 311 | if customer_id is None: 312 | customer_id = self.customer.id 313 | 314 | urn = "/v1/customers/{customer_id}/payment_methods".\ 315 | format(customer_id=customer_id) 316 | 317 | response = self.__conn.get(urn, data) 318 | payments = [] 319 | 320 | for payment in response: 321 | obj_payment = IuguPaymentMethod(self.customer, **payment) 322 | payments.append(obj_payment) 323 | 324 | return payments 325 | 326 | def set(self, payment_id, description, customer_id=None, 327 | set_as_default=False): 328 | """Updates/changes payment method with based in payment ID and customer. 329 | And returns object edited. 330 | 331 | HINT: Use save() to modify instances 332 | """ 333 | data = [] 334 | data.append(("description", description)) 335 | data.append(("set_as_default", set_as_default)) 336 | 337 | if customer_id is None: 338 | customer_id = self.customer.id 339 | 340 | urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ 341 | format(customer_id=customer_id, payment_id=payment_id) 342 | response = self.__conn.put(urn, data) 343 | 344 | return IuguPaymentMethod(self.customer, **response) 345 | 346 | def save(self): 347 | return self.set(self.id, self.description) 348 | 349 | 350 | def delete(self, payment_id, customer_id=None): 351 | """Deletes payment method with based in ID and customer. And 352 | returns object edited. 353 | 354 | HINT: Use remove() for to remove instances 355 | """ 356 | data = [] 357 | 358 | if customer_id is None: 359 | customer_id = self.customer.id 360 | 361 | urn = "/v1/customers/{customer_id}/payment_methods/{payment_id}".\ 362 | format(customer_id=customer_id, payment_id=payment_id) 363 | 364 | response = self.__conn.delete(urn, data) 365 | 366 | return IuguPaymentMethod(self.customer, **response) 367 | 368 | def remove(self): 369 | assert self.id is not None, "Invalid: IuguPaymentMethod not have ID." 370 | return self.delete(self.id) 371 | 372 | 373 | class PaymentTypeCreditCard(object): 374 | 375 | """ 376 | 377 | This class abstract the data parameter of payment method context 378 | 379 | :method is_valid: check if required fields is correct 380 | :method to_data: returns the data in format to be encoded by urllib as a 381 | list of tuples 382 | 383 | """ 384 | 385 | def __init__(self, **kwargs): 386 | self.number = kwargs.get('number') 387 | self.verification_value = kwargs.get('verification_value') 388 | self.first_name = kwargs.get('first_name') 389 | self.last_name = kwargs.get('last_name') 390 | self.month = kwargs.get('month') 391 | self.year = kwargs.get('year') 392 | self.display_number = kwargs.get('display_number') 393 | self.token = kwargs.get('token') 394 | self.brand = kwargs.get('brand') 395 | 396 | def is_valid(self): 397 | """Required to send to API""" 398 | if self.number and self.verification_value and self.first_name and \ 399 | self.last_name and self.month and self.year: 400 | return True 401 | else: 402 | return False 403 | 404 | def to_data(self): 405 | """ 406 | Returns a list of tuples with ("data[field]", value). Use it to 407 | return a data that will extend the data params in request. 408 | """ 409 | # control to required fields 410 | if not self.is_valid(): 411 | blanks = [ k for k, v in self.__dict__.items() if v is None] 412 | raise TypeError("All fields required to %s. Blank fields given %s" % 413 | (self.__class__, blanks)) 414 | 415 | data = [] 416 | for k, v in self.__dict__.items(): 417 | key = "data[{key_name}]".format(key_name=k) 418 | data.append((key, v)) 419 | 420 | return data 421 | -------------------------------------------------------------------------------- /lib/iugu/errors.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | class IuguConfigException(BaseException): 4 | 5 | def __init__(self, value="Required environment variable IUGU_API_TOKEN"): 6 | self.value = value 7 | 8 | def __str__(self): 9 | return repr(self.value) 10 | 11 | class IuguConfigTestsErrors(BaseException): 12 | 13 | def __init__(self, value="Required environment variables"): 14 | self.value = value 15 | 16 | def __str__(self): 17 | return repr(self.value) 18 | 19 | class IuguPaymentMethodException(BaseException): 20 | 21 | def __init__(self, value="Required customer_id must not be blank or None"): 22 | self.value = value 23 | 24 | def __str__(self): 25 | return repr(self.value) 26 | 27 | 28 | class IuguGeneralException(BaseException): 29 | 30 | def __init__(self, value="Data returned not matches with iugu-python classes"): 31 | self.value = value 32 | 33 | def __str__(self): 34 | return repr(self.value) 35 | 36 | 37 | class IuguInvoiceException(BaseException): 38 | 39 | def __init__(self, value="Incomplete request for create invoices"): 40 | self.value = value 41 | 42 | def __str__(self): 43 | return repr(self.value) 44 | 45 | 46 | class IuguPlansException(BaseException): 47 | 48 | def __init__(self, value="Incomplete request for create plans"): 49 | self.value = value 50 | 51 | def __str__(self): 52 | return repr(self.value) 53 | 54 | class IuguSubscriptionsException(BaseException): 55 | 56 | def __init__(self, value="Invalid request for Subscriptions"): 57 | self.value = value 58 | 59 | def __str__(self): 60 | return repr(self.value) 61 | -------------------------------------------------------------------------------- /lib/iugu/invoices.py: -------------------------------------------------------------------------------- 1 | __author__ = 'horacioibrahim' 2 | 3 | 4 | # python-iugu package modules 5 | import merchant, config, base, errors 6 | 7 | class IuguInvoice(base.IuguApi): 8 | 9 | """ 10 | 11 | This class allows handling invoices. The invoice is used to customers to 12 | make payments. 13 | 14 | :attribute class data: is a descriptor that carries rules of API fields. 15 | Only fields not None or not Blank can be sent. 16 | :attribute status: Accept two option: draft and pending, but can be 17 | draft, pending, [paid and canceled (internal use)] 18 | :attribute logs: is instanced a dictionary like JSON 19 | :attribute bank_slip: is instanced a dictionary like JSON 20 | 21 | => http://iugu.com/referencias/api#faturas 22 | """ 23 | 24 | __conn = base.IuguRequests() 25 | 26 | def __init__(self, item=None, **kwargs): 27 | super(IuguInvoice, self).__init__(**kwargs) 28 | self.id = kwargs.get("id") 29 | self.due_date = kwargs.get("due_date") 30 | self.currency = kwargs.get("currency") 31 | self.discount_cents = kwargs.get("discount_cents") 32 | # self.customer_email = kwargs.get("customer_email") 33 | self.email = kwargs.get("email") # customer email 34 | self.items_total_cents = kwargs.get("items_total_cents") 35 | self.notification_url = kwargs.get("notification_url") 36 | self.return_url = kwargs.get("return_url") 37 | self.status = kwargs.get("status") # [draft,pending] internal:[paid,canceled] 38 | self.expiration_url = kwargs.get("expiration_url") 39 | self.tax_cents = kwargs.get("tax_cents") 40 | self.updated_at = kwargs.get("updated_at") 41 | self.total_cents = kwargs.get("total_cents") 42 | self.paid_at = kwargs.get("paid_at") 43 | self.secure_id = kwargs.get("secure_id") 44 | self.secure_url = kwargs.get("secure_url") 45 | self.customer_id = kwargs.get("customer_id") 46 | self.user_id = kwargs.get("user_id") 47 | self.total = kwargs.get("total") 48 | self.created_at = kwargs.get("created_at") 49 | self.taxes_paid = kwargs.get("taxes_paid") 50 | self.interest = kwargs.get("interest") 51 | self.discount = kwargs.get("discount") 52 | self.refundable = kwargs.get("refundable") 53 | self.installments = kwargs.get("installments") 54 | self.bank_slip = kwargs.get("bank_slip") # TODO: create a class/object. 55 | self.logs = kwargs.get("logs") # TODO: create a class/object 56 | # TODO: descriptors (getter/setter) for items 57 | _items = kwargs.get("items") 58 | self.items = None 59 | 60 | if _items: 61 | # TODO: list comprehensions 62 | _list_items = [] 63 | for i in _items: 64 | obj_item = merchant.Item(**i) 65 | _list_items.append(obj_item) 66 | self.items = _list_items 67 | else: 68 | if item: 69 | assert isinstance(item, merchant.Item), "item must be instance of Item" 70 | self.items = item 71 | 72 | self.variables = kwargs.get("variables") 73 | self.logs = kwargs.get("logs") 74 | self.custom_variables = kwargs.get("custom_variables") 75 | self._data = None 76 | 77 | # constructor of data descriptors 78 | def data_get(self): 79 | return self._data 80 | 81 | def data_set(self, kwargs): 82 | draft = kwargs.get("draft") 83 | return_url = kwargs.get("return_url") 84 | expired_url = kwargs.get("expired_url") 85 | notification_url = kwargs.get("notification_url") 86 | tax_cents = kwargs.get("tax_cents") 87 | discount_cents = kwargs.get("discount_cents") 88 | customer_id = kwargs.get("customer_id") 89 | ignore_due_email = kwargs.get("ignore_due_email") 90 | subscription_id = kwargs.get("subscription_id") 91 | due_date = kwargs.get("due_date") 92 | credits = kwargs.get("credits") 93 | items = kwargs.get("items") 94 | email = kwargs.get("email") 95 | custom_data = kwargs.get("custom_data") 96 | 97 | data = [] 98 | 99 | if draft: 100 | data.append(("status", "draft")) # default is pending 101 | 102 | # data will posted and can't null, None or blank 103 | if return_url: 104 | self.return_url = return_url 105 | 106 | if self.return_url: 107 | data.append(("return_url", self.return_url)) 108 | 109 | if expired_url: 110 | self.expiration_url = expired_url 111 | 112 | if self.expiration_url: 113 | data.append(("expired_url", self.expiration_url)) 114 | 115 | if notification_url: 116 | self.notification_url = notification_url 117 | 118 | if self.notification_url: 119 | data.append(("notification_url", self.notification_url)) 120 | 121 | if tax_cents: 122 | self.tax_cents = tax_cents 123 | 124 | data.append(("tax_cents", self.tax_cents)) 125 | 126 | if discount_cents: 127 | self.discount_cents = discount_cents 128 | 129 | data.append(("discount_cents", self.discount_cents)) 130 | 131 | if customer_id: 132 | self.customer_id = customer_id 133 | 134 | if self.customer_id: 135 | data.append(("customer_id", self.customer_id)) 136 | 137 | if credits: 138 | data.append(("credits", credits)) 139 | 140 | if ignore_due_email: 141 | data.append(("ignore_due_email", True)) 142 | 143 | if subscription_id: 144 | data.append(("subscription_id", subscription_id)) 145 | 146 | if due_date: 147 | self.due_date = due_date 148 | 149 | if self.due_date: 150 | data.append(("due_date", self.due_date)) 151 | 152 | if isinstance(items, list): 153 | for item in items: 154 | data.extend(item.to_data()) 155 | else: 156 | if items is not None: 157 | data.extend(items.to_data()) 158 | 159 | if email: 160 | self.email = email 161 | 162 | if self.email: 163 | data.append(("email", self.email)) 164 | 165 | if custom_data: 166 | data.extend(custom_data) 167 | 168 | self._data = data 169 | 170 | def data_del(self): 171 | del self._data 172 | 173 | data = property(data_get, data_set, data_del, "data property set/get/del") 174 | 175 | def create(self, draft=False, return_url=None, email=None, expired_url=None, 176 | notification_url=None, tax_cents=None, discount_cents=None, 177 | customer_id=None, ignore_due_email=False, subscription_id=None, 178 | credits=None, due_date=None, items=None, custom_variables=None): 179 | """ 180 | Creates an invoice and returns owns class 181 | 182 | :param subscription_id: must be existent subscription from API 183 | :param customer_id: must be API customer_id (existent customer) 184 | :param items: must be item instance of merchant.Item() 185 | :para custom_variables: a dict {'key':'value'} 186 | 187 | => http://iugu.com/referencias/api#faturas 188 | """ 189 | urn = "/v1/invoices" 190 | 191 | # handling required fields 192 | if not due_date: 193 | if self.due_date: 194 | # due_date is required. If it not passed in args, it must to 195 | # exist at least in instance object 196 | due_date = self.due_date # "force" declaring locally 197 | else: 198 | raise errors.IuguInvoiceException(value="Required due_date is" \ 199 | " empty.") 200 | 201 | if not items: 202 | if self.items: 203 | # items are required. If it not passed as args, 204 | # it must to exist at least in instance object 205 | items = self.items # "force" declaring locally 206 | else: 207 | raise errors.IuguInvoiceException(value="Required items is" \ 208 | " empty.") 209 | 210 | if not email: 211 | if self.email: 212 | # email is required. If it not passed as args, 213 | # it must to exist at least in instance object 214 | email = self.email # "force" declaring locally 215 | else: 216 | raise errors.IuguInvoiceException(value="Required customer" \ 217 | " email is empty.") 218 | 219 | if custom_variables: 220 | custom_data = self.custom_variables_list(custom_variables) 221 | # to declare all variables local before calling locals().copy() 222 | kwargs_local = locals().copy() 223 | kwargs_local.pop('self') 224 | self.data = kwargs_local 225 | response = self.__conn.post(urn, self.data) 226 | invoice = IuguInvoice(**response) 227 | return invoice 228 | 229 | def set(self, invoice_id, email=None, due_date=None, 230 | return_url=None, expired_url=None, notification_url=None, 231 | tax_cents=None, discount_cents=None, customer_id=None, 232 | ignore_due_email=False, subscription_id=None, credits=None, 233 | items=None, custom_variables=None): 234 | """ Updates/changes a invoice that already exists 235 | 236 | :param custom_variables: a dict {'key', value}. If previously values 237 | exist the variable is edited rather is added 238 | 239 | IMPORTANT: Only invoices with status "draft" can be changed all fields 240 | otherwise (if status pending, cancel or paid) only the field logs 241 | can to change. 242 | """ 243 | urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) 244 | 245 | if items is not None: 246 | assert isinstance(items, merchant.Item), "item must be instance of Item" 247 | 248 | if custom_variables: 249 | custom_data = self.custom_variables_list(custom_variables) 250 | # to declare all variables local before calling locals().copy() 251 | kwargs_local = locals().copy() 252 | kwargs_local.pop('self') 253 | self.data = kwargs_local 254 | response = self.__conn.put(urn, self.data) 255 | 256 | return IuguInvoice(**response) 257 | 258 | def save(self): 259 | """Save updating a invoice's instance. To add/change custom_variables 260 | keywords to use create() or set() 261 | 262 | IMPORTANT: Only invoices with status "draft" can be changed 263 | """ 264 | self.data = self.__dict__ 265 | urn = "/v1/invoices/{invoice_id}".format(invoice_id=self.id) 266 | response = self.__conn.put(urn, self.data) 267 | 268 | return IuguInvoice(**response) 269 | 270 | @classmethod 271 | def get(self, invoice_id): 272 | """Gets one invoice with base in invoice_id and returns instance""" 273 | data = [] 274 | urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) 275 | response = self.__conn.get(urn, data) 276 | 277 | return IuguInvoice(**response) 278 | 279 | @classmethod 280 | def getitems(self, limit=None, skip=None, created_at_from=None, 281 | created_at_to=None, query=None, updated_since=None, sort=None, 282 | customer_id=None): 283 | """ 284 | Gets a list of invoices where the API default is limited 100. Returns 285 | a list of IuguInvoice 286 | 287 | :param limit: limits the number of invoices returned by API 288 | :param skip: skips a numbers of invoices where more recent insert 289 | ordering. Useful to pagination. 290 | :param query: filters based in value (case insensitive) 291 | :param sort: sorts based in field. Use minus signal to determine the 292 | direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API 293 | :return: list of IuguInvoice instances 294 | """ 295 | data = [] 296 | urn = "/v1/invoices/" 297 | 298 | # Set options 299 | if limit: 300 | data.append(("limit", limit)) 301 | if skip: 302 | data.append(("start", skip)) 303 | if created_at_from: 304 | data.append(("created_at_from", created_at_from)) 305 | if created_at_to: 306 | data.append(("created_at_to", created_at_to)) 307 | if updated_since: 308 | data.append(("updated_since", updated_since)) 309 | if query: 310 | data.append(("query", query)) 311 | if customer_id: 312 | data.append(("customer_id", customer_id)) 313 | 314 | # TODO: sort not work fine. Waiting support of API providers 315 | if sort: 316 | assert sort is not str, "sort must be string as -name or name" 317 | 318 | if sort.startswith("-"): 319 | sort = sort[1:] 320 | key = "sortBy[{field}]".format(field=sort) 321 | data.append((key, "desc")) 322 | else: 323 | key = "sortBy[{field}]".format(field=sort) 324 | data.append((key, "asc")) 325 | 326 | invoices = self.__conn.get(urn, data) 327 | #TODO: list comprehensions 328 | invoices_objects = [] 329 | for invoice_item in invoices["items"]: 330 | obj_invoice = IuguInvoice(**invoice_item) 331 | invoices_objects.append(obj_invoice) 332 | 333 | return invoices_objects 334 | 335 | def remove(self, invoice_id=None): 336 | """ 337 | Removes an invoice by id or instance and returns None 338 | """ 339 | invoice_id = invoice_id if invoice_id else self.id 340 | if invoice_id is None: 341 | raise errors.IuguSubscriptionsException(value="ID (invoice_id) can't be empty") 342 | 343 | urn = "/v1/invoices/{invoice_id}".format(invoice_id=invoice_id) 344 | response = self.__conn.delete(urn, []) 345 | obj = IuguInvoice(**response) 346 | # TODO: list comprehensions ? 347 | if obj: 348 | for k, v in self.__dict__.items(): 349 | self.__dict__[k] = None 350 | 351 | def cancel(self): 352 | """Cancels an instance of invoice and returns own invoice with status 353 | canceled""" 354 | urn = "/v1/invoices/{invoice_id}/cancel".format(invoice_id=self.id) 355 | 356 | # This below if to avoid a request because the API not allow this operation 357 | # but all API can to change theirs behaviors so to allow to cancel 358 | # invoices with status difference of "pending". 359 | # The approach without if also to raise exception with error from directly 360 | # API responses but here the focus is less requests. 361 | if self.status == "pending": 362 | response = self.__conn.put(urn, []) 363 | obj = IuguInvoice(**response) 364 | else: 365 | raise errors.IuguGeneralException(value="Cancel operation support only " \ 366 | "invoices with status: pending.") 367 | 368 | return obj 369 | 370 | @classmethod 371 | def to_cancel(self, invoice_id): 372 | """Cancels an invoice with base in invoice ID and returns own 373 | invoice with status canceled 374 | 375 | => http://iugu.com/referencias/api#cancelar-uma-fatura 376 | """ 377 | urn = "/v1/invoices/{invoice_id}/cancel".format(invoice_id=invoice_id) 378 | response = self.__conn.put(urn, []) 379 | obj = IuguInvoice(**response) 380 | 381 | return obj 382 | 383 | def refund(self): 384 | """Makes refund of an instance of invoice 385 | 386 | => http://iugu.com/referencias/api#reembolsar-uma-fatura 387 | """ 388 | urn = "/v1/invoices/{invoice_id}/refund".format(invoice_id=self.id) 389 | 390 | # This below if to avoid a request because the API not allow this operation 391 | # but all API can to change theirs behaviors so to allow to refund 392 | # invoices with status difference of "paid". 393 | # The approach without if also to raise exception with error from directly 394 | # API responses but here the focus is less requests. 395 | if self.status == "paid": 396 | response = self.__conn.post(urn, []) 397 | obj = IuguInvoice(**response) 398 | else: 399 | raise errors.IuguGeneralException(value="Refund operation support only " \ 400 | "invoices with status: paid.") 401 | 402 | return obj 403 | -------------------------------------------------------------------------------- /lib/iugu/merchant.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import httplib 4 | import json 5 | from urllib import urlencode 6 | from urllib2 import urlopen, Request 7 | 8 | # python-iugu package modules 9 | import base 10 | import config 11 | 12 | 13 | class IuguMerchant(base.IuguApi): 14 | 15 | def __init__(self, **kwargs): 16 | super(IuguMerchant, self).__init__(**kwargs) 17 | self.__conn = base.IuguRequests() 18 | 19 | def create_payment_token(self, card_number, first_name, last_name, 20 | month, year, verification_value, method="credit_card"): 21 | """Sends credit_card data of a customer and returns a token 22 | for payment process without needing to persist personal data 23 | of customers. 24 | 25 | :param method: string 'credit_card' or options given by API. 26 | :param card_number: str of card number 27 | :param first_name: string with consumer/buyer first name 28 | :param last_name: consumer/buyer last name 29 | :param month: two digits to Month expiry date of card 30 | :param year: four digits to Year expiry date of card 31 | :param verification_value: CVV 32 | :returns: token_id as id, response, extra_info and method 33 | 34 | => http://iugu.com/referencias/api#tokens-e-cobranca-direta 35 | """ 36 | urn = "/v1/payment_token" 37 | data = [('data[last_name]', last_name), ('data[first_name]', first_name), 38 | ('data[verification_value]', verification_value), 39 | ('data[month]', month), ('data[year]', year), 40 | ('data[number]', card_number)] 41 | 42 | data.append(("account_id", self.account_id)) # work less this 43 | data.append(("test", self.is_mode_test())) 44 | data.append(("method", method)) 45 | token_data = self.__conn.post(urn, data) 46 | 47 | return Token(token_data) 48 | 49 | def create_charge(self, consumer_email, items, token=None, payer=None): 50 | """ 51 | Creates an invoice and returns a direct charge done. 52 | 53 | :param items: is instance of class of the merchant.Item 54 | :param token: an instance of Token. It's used to credit card payments. 55 | If argument token is None it's used to method=bank_slip 56 | """ 57 | data = [] # data fields of charge. It'll encode 58 | urn = "/v1/charge" 59 | 60 | if isinstance(items, list): 61 | for item in items: 62 | assert type(item) is Item 63 | data.extend(item.to_data()) 64 | else: 65 | assert type(items) is Item 66 | data.extend(items.to_data()) 67 | 68 | if token and isinstance(token, Token): 69 | token_id = token.id 70 | data.append(("token", token_id)) 71 | else: 72 | data.append(("method", "bank_slip")) 73 | 74 | if payer is not None: 75 | assert type(payer) is Payer 76 | data.extend(payer.to_data()) 77 | 78 | data.append(("email", consumer_email)) 79 | results = self.__conn.post(urn, data) 80 | 81 | return Charge(results) 82 | 83 | 84 | class Charge(object): 85 | 86 | """ 87 | This class receives response of request create_charge. Useful only to view 88 | status and invoice_id 89 | 90 | :attribute invoice_id: ID of Invoice created 91 | 92 | """ 93 | 94 | def __init__(self, invoice): 95 | self.invoice = invoice 96 | 97 | if 'message' in invoice: 98 | self.message = invoice['message'] 99 | 100 | if 'errors' in invoice: 101 | self.errors = invoice['errors'] 102 | 103 | if 'success' in invoice: 104 | self.success = invoice['success'] 105 | 106 | if 'invoice_id' in invoice: 107 | self.invoice_id = invoice['invoice_id'] 108 | 109 | def is_success(self): 110 | try: 111 | if self.success == True: 112 | return True 113 | except: 114 | pass 115 | 116 | return False 117 | 118 | 119 | class Token(object): 120 | 121 | """ 122 | 123 | This class is representation of payment method to API. 124 | 125 | """ 126 | 127 | def __init__(self, token_data): 128 | self.token_data = token_data 129 | 130 | if 'id' in token_data: 131 | self.id = token_data['id'] 132 | if 'extra_info' in token_data: 133 | self.extra_info = token_data['extra_info'] 134 | if 'method' in token_data: 135 | self.method = token_data['method'] 136 | 137 | @property 138 | def is_test(self): 139 | if 'test' in self.token_data.keys() and self.token_data['test'] == True: 140 | return True 141 | else: 142 | return False 143 | 144 | @property 145 | def status(self): 146 | try: 147 | if 'errors' in self.token_data.keys(): 148 | return self.token_data['errors'] 149 | except: 150 | pass 151 | 152 | return 200 153 | 154 | 155 | class Payer(object): 156 | 157 | def __init__(self, name, email, address=None, cpf_cnpj=None, phone_prefix=None, phone=None): 158 | self.cpf_cnpj = cpf_cnpj 159 | self.name = name 160 | self.email = email 161 | self.phone_prefix = phone_prefix 162 | self.phone = phone 163 | 164 | if isinstance(address, Address): 165 | self.address = address 166 | 167 | def to_data(self): 168 | """ 169 | Returns tuples to encode with urllib.urlencode 170 | """ 171 | as_tuple = [] 172 | key = "payer" 173 | 174 | as_tuple.append(("{payer}[cpf_cnpj]".format( 175 | payer=key), self.cpf_cnpj)) 176 | as_tuple.append(("{payer}[name]".format(payer=key), self.name)) 177 | as_tuple.append(("{payer}[email]".format(payer=key), self.email)) 178 | 179 | if self.address: 180 | as_tuple.append(("{payer}[address.zip_code]".format( 181 | payer=key), self.address.zip_code)) 182 | as_tuple.append(("{payer}[address.number]".format( 183 | payer=key), self.address.number)) 184 | 185 | return as_tuple 186 | 187 | 188 | class Address(object): 189 | 190 | def __init__(self, street, number, city, state, country, zip_code): 191 | self.street = street 192 | self.number = number 193 | self.city = city 194 | self.state = state 195 | self.country = country 196 | self.zip_code = zip_code 197 | 198 | 199 | class Item(object): 200 | """ 201 | This class represent a checkout item. It's used to create a charge, mainly, 202 | within IuguMerchant class. 203 | """ 204 | 205 | def __init__(self, description, quantity, price_cents, **kwargs): 206 | self.description = description 207 | self.quantity = quantity 208 | self.price_cents = price_cents # must be integer 10.90 => 1090 209 | self.id = kwargs.get("id") 210 | self.created_at = kwargs.get("created_at") 211 | self.updated_at = kwargs.get("updated_at") 212 | self.price = kwargs.get("price") 213 | # useful for subscriptions subitems 214 | self.recurrent = kwargs.get("recurrent") # boolean 215 | self.total = kwargs.get("total") 216 | # command for eliminate an item 217 | self.destroy = None 218 | 219 | def __str__(self): 220 | return "%s" % self.description 221 | 222 | def to_data(self, is_subscription=False): 223 | """ 224 | Returns tuples to encode with urllib.urlencode 225 | """ 226 | as_tuple = [] 227 | key = "items" 228 | 229 | if is_subscription is True: 230 | key = "subitems" # run to adapt the API subscription 231 | 232 | if self.id: 233 | as_tuple.append(("{items}[][id]".format(items=key), self.id)) 234 | 235 | as_tuple.append(("{items}[][description]".format(items=key), 236 | self.description)) 237 | as_tuple.append(("{items}[][quantity]".format(items=key), 238 | self.quantity)) 239 | as_tuple.append(("{items}[][price_cents]".format(items=key), 240 | self.price_cents)) 241 | 242 | if self.recurrent: 243 | value_recurrent = str(self.recurrent) 244 | value_recurrent = value_recurrent.lower() 245 | as_tuple.append(("{items}[][recurrent]".format(items=key), 246 | value_recurrent)) 247 | 248 | if self.destroy is not None: 249 | value_destroy = str(self.destroy) 250 | value_destroy = value_destroy.lower() 251 | as_tuple.append(("{items}[][_destroy]".format(items=key), 252 | value_destroy)) 253 | 254 | return as_tuple 255 | 256 | def remove(self): 257 | """ 258 | Marks the item that will removed after save an invoice 259 | """ 260 | self.destroy = True 261 | 262 | 263 | class Transfers(object): 264 | 265 | __conn = base.IuguRequests() 266 | __urn = "/v1/transfers" 267 | 268 | def __init__(self, **kwargs): 269 | self.id = kwargs.get("id") 270 | self.created_at = kwargs.get("created_at") 271 | self.amount_cents = kwargs.get("amount_cents") 272 | self.amount_localized = kwargs.get("amount_localized") 273 | self.receiver = kwargs.get("receiver") 274 | self.sender = kwargs.get("sender") 275 | 276 | def send(self, receiver_id, amount_cents): 277 | """ 278 | To send amount_cents to receiver_id 279 | """ 280 | data = [] 281 | data.append(("receiver_id", receiver_id)) 282 | data.append(("amount_cents", amount_cents)) 283 | response = self.__conn.post(self.__urn, data) 284 | return Transfers(**response) 285 | 286 | @classmethod 287 | def getitems(self): 288 | """ 289 | Gets sent and received transfers for use in API_KEY 290 | """ 291 | response = self.__conn.get(self.__urn, []) 292 | sent = response["sent"] 293 | received = response["received"] 294 | transfers = [] 295 | 296 | for t in sent: 297 | transfer_obj = Transfers(**t) 298 | transfers.append(transfer_obj) 299 | 300 | for r in received: 301 | transfer_obj = Transfers(**r) 302 | transfers.append(transfer_obj) 303 | 304 | return transfers 305 | -------------------------------------------------------------------------------- /lib/iugu/plans.py: -------------------------------------------------------------------------------- 1 | __author__ = 'horacioibrahim' 2 | 3 | # python-iugu package modules 4 | import base, config, errors 5 | 6 | class IuguPlan(object): 7 | 8 | """ 9 | 10 | This class allows handling plans. Basically contains a CRUD 11 | 12 | :attribute data: is a descriptor and their setters carries the rules 13 | 14 | => http://iugu.com/referencias/api#criar-um-plano 15 | 16 | """ 17 | 18 | __conn = base.IuguRequests() 19 | 20 | def __init__(self, **kwargs): 21 | self.id = kwargs.get("id") 22 | self.name = kwargs.get("name") 23 | self.identifier = kwargs.get("identifier") 24 | self.interval = kwargs.get("interval") 25 | self.interval_type = kwargs.get("interval_type") 26 | self.created_at = kwargs.get("created_at") 27 | self.updated_at = kwargs.get("updated_at") 28 | self.currency = kwargs.get("currency") # API move it to prices scope 29 | self.value_cents = kwargs.get("value_cents") # API move it to prices scope 30 | self._data = None 31 | self._prices = kwargs.get("prices") 32 | self.prices = [] 33 | self._features = kwargs.get("features") 34 | self.features = [] 35 | 36 | if isinstance(self._prices, list): 37 | for price in self._prices: 38 | obj_price = Price(**price) 39 | self.prices.append(obj_price) 40 | 41 | if isinstance(self._features, list): 42 | for feature in self._features: 43 | obj_feature = Feature(**feature) 44 | self.features.append(obj_feature) 45 | 46 | def is_valid(self): 47 | """Checks required fields to send to API. 48 | 49 | IMPORTANT: Only to use before send request for API. The fields currency 50 | and value_cents will saved in prices scope. Because not to use validate 51 | with returned data by API. 52 | """ 53 | 54 | if self.name and self.identifier and self.interval and \ 55 | self.interval_type and self.currency and self.value_cents: 56 | return True 57 | else: 58 | return False 59 | 60 | @property 61 | def data(self): 62 | return self._data 63 | 64 | @data.setter 65 | def data(self, kwargs): 66 | """Defines data and validates required fields to send to API. 67 | Returns data as list for urlencoded. 68 | """ 69 | data = [] 70 | 71 | # required fields 72 | self.name = kwargs.get("name") 73 | self.identifier = kwargs.get("identifier") 74 | self.interval = kwargs.get("interval") 75 | self.interval_type = kwargs.get("interval_type") 76 | self.currency = kwargs.get("currency") 77 | self.value_cents = kwargs.get("value_cents") 78 | # optional fields 79 | self.prices = kwargs.get("prices") 80 | self.features = kwargs.get("features") 81 | 82 | # required fields. if not passed the API return an exception 83 | if self.name: 84 | data.append(("name", self.name)) 85 | 86 | if self.identifier: 87 | data.append(("identifier", self.identifier)) 88 | 89 | if self.interval: 90 | data.append(("interval", self.interval)) 91 | 92 | if self.interval_type: 93 | data.append(("interval_type", self.interval_type)) 94 | 95 | if self.currency: 96 | if self.currency == "BRL": 97 | data.append(("currency", self.currency)) 98 | else: 99 | raise errors.IuguPlansException(value="Only BRL supported") 100 | 101 | if self.value_cents: 102 | data.append(("value_cents", self.value_cents)) 103 | 104 | # optional fields 105 | if self.prices: 106 | if isinstance(self.prices, list): 107 | # each prices items must be instance's Price class 108 | for price in self.prices: 109 | data.extend(price.to_data()) 110 | else: 111 | raise errors.IuguPlansException(value="The fields prices must "\ 112 | "be a list of obj Price") 113 | 114 | if self.features: 115 | if isinstance(self.features, list): 116 | for feature in self.features: 117 | data.extend(feature.to_data()) 118 | else: 119 | raise errors.IuguPlansException(value="The fields features " \ 120 | "must be a list of obj Feature") 121 | 122 | self._data = data 123 | 124 | @data.deleter 125 | def data(self): 126 | del self._data 127 | 128 | def create(self, name=None, identifier=None, interval=None, 129 | interval_type=None, currency=None, value_cents=None, 130 | features=None, prices=None): 131 | """ 132 | Creates a new plans in API and returns an IuguPlan's instance. The 133 | fields required are name, identifier, interval, interval_type and 134 | values_cents. 135 | 136 | :param name: name of a plan 137 | :param identifier: unique name identifier in API plan context 138 | :param interval: an integer that define duration (e.g 12 to one year) 139 | :param interval_type: a string with "weeks" or "months" 140 | :param currency: only support BRL. If different raise exception 141 | :param value_cents: an integer with price in cents (e.g 1000 > 10.00) 142 | :param prices: a list of prices. The definition in API is obscure 143 | :param features: details with features that must be a list with 144 | instance of Features 145 | """ 146 | urn = "/v1/plans" 147 | 148 | if not name: 149 | if self.name: 150 | name = self.name 151 | else: 152 | raise errors.IuguPlansException(value="Name is required") 153 | 154 | if not identifier: 155 | if self.identifier: 156 | identifier = self.identifier 157 | else: 158 | raise errors.IuguPlansException(value="identifier is required") 159 | 160 | if not interval: 161 | if self.interval: 162 | interval = self.interval 163 | else: 164 | raise errors.IuguPlansException(value="interval is required") 165 | 166 | if not interval_type: 167 | if self.interval_type: 168 | interval_type = self.interval_type 169 | else: 170 | raise errors.IuguPlansException(value="interval_type is required") 171 | 172 | if not features: 173 | if self.features: 174 | features = self.features 175 | 176 | if not prices: 177 | if self.prices: 178 | prices = self.prices 179 | 180 | if not value_cents: 181 | if self.value_cents: 182 | value_cents = self.value_cents 183 | else: 184 | raise errors.IuguPlansException(value="value_cents is required") 185 | 186 | if not currency: 187 | if self.currency: 188 | currency = self.currency 189 | 190 | kwargs_local = locals().copy() 191 | kwargs_local.pop('self') # prevent error of multiple value for args 192 | self.data = kwargs_local 193 | response = self.__conn.post(urn, self.data) 194 | 195 | return IuguPlan(**response) 196 | 197 | def set(self, plan_id, name=None, identifier=None, interval=None, 198 | interval_type=None, currency=None, value_cents=None, 199 | features=None, prices=None): 200 | """ 201 | Edits/changes existent plan and returns IuguPlan's instance 202 | 203 | :param plan_id: ID number of a existent plan 204 | """ 205 | urn = "/v1/plans/{plan_id}".format(plan_id=plan_id) 206 | kwargs_local = locals().copy() 207 | kwargs_local.pop('self') 208 | self.data = kwargs_local 209 | response = self.__conn.put(urn, self.data) 210 | return IuguPlan(**response) 211 | 212 | def save(self): 213 | """Saves an instance of IuguPlan and return own class instance 214 | modified""" 215 | urn = "/v1/plans/{plan_id}".format(plan_id=self.id) 216 | self.data = self.__dict__ 217 | response = self.__conn.put(urn, self.data) 218 | return IuguPlan(**response) 219 | 220 | @classmethod 221 | def get(self, plan_id): 222 | """Gets one plan based in ID and returns an instance""" 223 | data = [] 224 | urn = "/v1/plans/{plan_id}".format(plan_id=plan_id) 225 | response = self.__conn.get(urn, data) 226 | return IuguPlan(**response) 227 | 228 | @classmethod 229 | def get_by_identifier(self, identifier): 230 | """Gets one plan based in identifier and returns an instance 231 | 232 | :param identifier: it's an unique identifier plan in API 233 | """ 234 | data = [] 235 | urn = "/v1/plans/identifier/{identifier}".format(identifier=identifier) 236 | response = self.__conn.get(urn, data) 237 | return IuguPlan(**response) 238 | 239 | @classmethod 240 | def getitems(self, limit=None, skip=None, query=None, updated_since=None, 241 | sort=None): 242 | """ 243 | Gets plans by API default limited 100. 244 | 245 | :param limit: limits the number of plans returned by API (default 246 | and immutable of API is 100) 247 | :param skip: skips a numbers of plans where more recent insert 248 | ordering. Useful to pagination. 249 | :param query: filters based in value (case insensitive) 250 | :param sort: sorts based in field. Use minus signal to determine the 251 | direction DESC or ASC (e.g sort="-email"). IMPORTANT: not work by API 252 | :return: list of IuguPlan's instances 253 | """ 254 | data = [] 255 | urn = "/v1/plans/" 256 | 257 | # Set options 258 | if limit: 259 | data.append(("limit", limit)) 260 | 261 | if skip: 262 | data.append(("start", skip)) 263 | 264 | if updated_since: 265 | data.append(("updated_since", updated_since)) 266 | 267 | if query: 268 | data.append(("query", query)) 269 | 270 | # TODO: sort not work fine. Waiting support of API providers 271 | if sort: 272 | assert sort is not str, "sort must be string as -name or name" 273 | 274 | if sort.startswith("-"): 275 | sort = sort[1:] 276 | key = "sortBy[{field}]".format(field=sort) 277 | data.append((key, "desc")) 278 | else: 279 | key = "sortBy[{field}]".format(field=sort) 280 | data.append((key, "asc")) 281 | 282 | plans = self.__conn.get(urn, data) 283 | plans_objects = [] 284 | for plan_item in plans["items"]: 285 | obj_plan = IuguPlan(**plan_item) 286 | plans_objects.append(obj_plan) 287 | 288 | return plans_objects 289 | 290 | def remove(self, plan_id=None): 291 | """ 292 | Removes an instance or passing a plan_id 293 | """ 294 | if plan_id: 295 | to_remove = plan_id 296 | else: 297 | to_remove = self.id 298 | 299 | if not to_remove: 300 | raise errors.IuguPlansException(value="Instance or plan id is required") 301 | 302 | urn = "/v1/plans/{plan_id}".format(plan_id=to_remove) 303 | response = self.__conn.delete(urn, []) 304 | # check if result can to generate instance of IuguPlan 305 | obj = IuguPlan(**response) 306 | 307 | if obj: 308 | for k, v in self.__dict__.items(): 309 | self.__dict__[k] = None 310 | 311 | 312 | class Price(object): 313 | 314 | """ 315 | 316 | This class is useful for handling field prices of API. Prices in API is a 317 | field of plans context it contains list of values with some fields 318 | exclusively returned by API. 319 | 320 | :method is_valid: check if required fields are correct 321 | :method to_data: returns a list of tuples for urlencoded 322 | 323 | """ 324 | 325 | def __init__(self, **kwargs): 326 | self.id = kwargs.get("id") 327 | self.plan_id = kwargs.get("plan_id") 328 | self.created_at = kwargs.get("created_at") 329 | self.updated_at = kwargs.get("updated_at") 330 | self.value_cents = kwargs.get("value_cents") 331 | self.currency = kwargs.get("currency") 332 | 333 | def is_valid(self): 334 | """Required fields to send to API""" 335 | if self.value_cents and self.currency: 336 | return True 337 | else: 338 | return False 339 | 340 | def to_data(self): 341 | """ 342 | Returns a list of tuples with ("prices[field]", value). Use it to 343 | return a data that will extend the data params in request. 344 | """ 345 | if not self.is_valid(): 346 | blanks = [ k for k, v in self.__dict__.items() if v is None] 347 | raise TypeError("All fields are required to %s. Blanks fields given %s" % 348 | (self.__class__, blanks)) 349 | 350 | data = [] 351 | for k, v in self.__dict__.items(): 352 | if v is not None: 353 | key = "prices[][{key_name}]".format(key_name=k) 354 | data.append((key, v)) 355 | 356 | return data 357 | 358 | 359 | class Feature(object): 360 | 361 | """ 362 | 363 | This class abstract features of Plan context. 364 | 365 | :method is_valid: check if required fields are correct 366 | :method to_data: returns a list of tuples for urlencoded 367 | """ 368 | 369 | def __init__(self, **kwargs): 370 | self.id = kwargs.get("id") 371 | self.identifier = kwargs.get("identifier") 372 | self.important = kwargs.get("important") 373 | self.name = kwargs.get("name") 374 | self.plan_id = kwargs.get("plan_id") 375 | self.position = kwargs.get("position") 376 | self.created_at = kwargs.get("created_at") 377 | self.updated_at = kwargs.get("updated_at") 378 | self.value = kwargs.get("value") 379 | 380 | def is_valid(self): 381 | """ 382 | Required to send to API 383 | """ 384 | if self.name and self.identifier and self.value > 0: 385 | return True 386 | else: 387 | return False 388 | 389 | def to_data(self): 390 | """ 391 | Returns a list of tuples with ("features[field]", value). Use it to 392 | return a data that will extend the data params in request. 393 | """ 394 | if not self.is_valid(): 395 | blanks = [ k for k, v in self.__dict__.items() if v is None ] 396 | raise TypeError("All fields are required to class %s. Blanks fields given %s" % 397 | (self.__class__, blanks)) 398 | 399 | data = [] 400 | for k, v in self.__dict__.items(): 401 | if v is not None: 402 | key = "features[][{key_name}]".format(key_name=k) 403 | data.append((key, v)) 404 | return data 405 | -------------------------------------------------------------------------------- /lib/iugu/subscriptions.py: -------------------------------------------------------------------------------- 1 | __author__ = 'horacioibrahim' 2 | 3 | # python-iugu package modules 4 | import base, config, errors, merchant 5 | 6 | class IuguSubscription(base.IuguApi): 7 | 8 | """ 9 | 10 | This class allows handling subscriptions an CRUD with create, get, set, 11 | save and remove plus add-ons as getitems, suspend, activate, change_plan, 12 | is_credit_based. 13 | 14 | :attribute class data: is a description it carries rules of data to API 15 | """ 16 | 17 | _conn = base.IuguRequests() 18 | 19 | def __init__(self, **kwargs): 20 | super(IuguSubscription, self).__init__(**kwargs) 21 | self.id = kwargs.get("id") 22 | # required 23 | self.customer_id = kwargs.get("customer_id") 24 | # optionals 25 | self.plan_identifier = kwargs.get("plan_identifier") # only credits_based subscriptions 26 | self.expires_at = kwargs.get("expires_at") 27 | # self.only_on_charge_success = kwargs.get("only_on_charge_success") # if exist payment method for client 28 | self._subitems = kwargs.get("subitems") 29 | self.subitems = [] # of items 30 | self.custom_variables = kwargs.get("custom_variables") 31 | self._data = None 32 | self.suspended = kwargs.get("suspended") 33 | self.price_cents = kwargs.get("price_cents") 34 | self.currency = kwargs.get("currency") 35 | # created by api 36 | self.created_at = kwargs.get("created_at") 37 | self.updated_at = kwargs.get("updated_at") 38 | self.customer_name = kwargs.get("customer_name") 39 | self.customer_email = kwargs.get("customer_email") 40 | self.cycled_at = kwargs.get("cycled_at") 41 | self.plan_name = kwargs.get("plan_name") 42 | self.customer_ref = kwargs.get("customer_ref") 43 | self.plan_ref = kwargs.get("plan_ref") 44 | self.active = kwargs.get("active") 45 | self.in_trial = kwargs.get("in_trial") 46 | self.recent_invoices = kwargs.get("recent_invoices") # only resume of invoice 47 | self.logs = kwargs.get("logs") 48 | self._type = kwargs.get("_type") # facilities to verify if credit_base or general 49 | 50 | if isinstance(self._subitems, list): 51 | for item in self._subitems: 52 | obj_item = merchant.Item(**item) 53 | self.subitems.append(obj_item) 54 | 55 | @staticmethod 56 | def is_credit_based(response): 57 | # Checks if HTTP response of API subscription is credit_based type 58 | if "credits_based" in response and response["credits_based"] == True: 59 | return True 60 | return False 61 | 62 | @property 63 | def data(self): 64 | return self._data 65 | 66 | @data.setter 67 | def data(self, kwargs): 68 | """ 69 | Body data for request send 70 | """ 71 | data = [] 72 | self.id = kwargs.get("sid") 73 | self.customer_id = kwargs.get("customer_id") 74 | self.plan_identifier = kwargs.get("plan_identifier") 75 | self.expires_at = kwargs.get("expires_at") 76 | self.only_on_charge_success = kwargs.get("only_on_charge_success") 77 | self.subitems = kwargs.get("subitems") 78 | self.custom_variables = kwargs.get("custom_data") 79 | self.credits_based = kwargs.get("credits_based") 80 | self.credits_min = kwargs.get("credits_min") 81 | self.credits_cycle = kwargs.get("credits_cycle") 82 | self.price_cents = kwargs.get("price_cents") 83 | self.suspended = kwargs.get("suspended") 84 | self.skip_charge = kwargs.get("skip_charge") 85 | 86 | if self.id: 87 | data.append(("id", self.id)) 88 | 89 | if self.customer_id: 90 | data.append(("customer_id", self.customer_id)) 91 | 92 | if self.plan_identifier: 93 | data.append(("plan_identifier", self.plan_identifier)) 94 | 95 | if self.expires_at: 96 | data.append(("expires_at", self.expires_at)) 97 | 98 | if self.only_on_charge_success: 99 | value_charge_success = str(self.only_on_charge_success) 100 | value_charge_success = value_charge_success.lower() 101 | data.append(("only_on_charge_success", value_charge_success)) 102 | 103 | if self.subitems: 104 | if isinstance(self.subitems, list): 105 | for item in self.subitems: 106 | data.extend((item.to_data(is_subscription=True))) 107 | else: 108 | raise errors.IuguSubscriptionsException("The subitems must be " \ 109 | "a list of obj Item") 110 | 111 | if self.custom_variables: # TODO: to create test 112 | data.extend(self.custom_variables) 113 | 114 | # credit based subscriptions 115 | if self.credits_based is not None: 116 | value_credits_based = str(self.credits_based) 117 | value_credits_based = value_credits_based.lower() 118 | data.append(("credits_based", value_credits_based)) 119 | 120 | if self.credits_min: 121 | data.append(("credits_min", self.credits_min)) 122 | 123 | if self.credits_cycle: 124 | data.append(("credits_cycle", self.credits_cycle)) 125 | 126 | if self.price_cents: 127 | data.append(("price_cents", self.price_cents)) 128 | 129 | if self.suspended is not None: 130 | value_suspended = str(self.suspended) 131 | value_suspended = value_suspended.lower() 132 | data.append(("suspended", value_suspended)) 133 | 134 | if self.skip_charge is not None: 135 | value_skip_charge = str(self.skip_charge) 136 | value_skip_charge = value_skip_charge.lower() 137 | data.append(("skip_charge", value_skip_charge)) 138 | 139 | self._data = data 140 | 141 | @data.deleter 142 | def data(self): 143 | del self._data 144 | 145 | def create(self, customer_id, plan_identifier, expires_at=None, 146 | only_on_charge_success=False, subitems=None, 147 | custom_variables=None): 148 | """ 149 | Creates new subscription 150 | 151 | :param customer_id: the ID of an existent customer 152 | :param plan_identifier: the identifier of a plan (it's not ID) 153 | :param expires_at: a string with expiration date and next charge (e.g 154 | "DD/MM/YYYY" or "31/12/2014") 155 | :param only_on_charge_success: creates the subscriptions if charged 156 | with success. It's supported if customer already have payment method 157 | inserted 158 | :param subitems: items of subscriptions 159 | 160 | => http://iugu.com/referencias/api#criar-uma-assinatura 161 | """ 162 | urn = "/v1/subscriptions" 163 | if custom_variables: 164 | assert isinstance(custom_variables, dict), "Required a dict" 165 | custom_data = self.custom_variables_list(custom_variables) 166 | kwargs_local = locals().copy() 167 | kwargs_local.pop('self') 168 | self.data = kwargs_local 169 | response = self._conn.post(urn, self.data) 170 | return IuguSubscription(**response) 171 | 172 | def set(self, sid, plan_identifier=None, expires_at=None, 173 | subitems=None, suspended=None, skip_charge=None, 174 | custom_variables=None, customer_id=None): 175 | """ 176 | Changes a subscriptions with based arguments and Returns modified 177 | subscription of type no credit_based. 178 | 179 | :param sid: ID of an existent subscriptions in API 180 | :param customer_id: ID of customer 181 | :param expires_at: expiration date and date of next charge 182 | :param subitems: subitems 183 | :param suspended: boolean to change status of subscription 184 | :param skip_charge: ignore charge. Bit explanation and obscure in API 185 | :param custom_variables: a dictionary {'key': 'value'} 186 | 187 | IMPORTANT 1: Removed parameter customer_id. Iugu's support (number 782) 188 | says that to change only customer_id isn't supported by API. 189 | """ 190 | urn = "/v1/subscriptions/{sid}".format(sid=sid) 191 | if custom_variables: 192 | assert isinstance(custom_variables, dict), "Required a dict" 193 | custom_data = self.custom_variables_list(custom_variables) 194 | kwargs_local = locals().copy() 195 | kwargs_local.pop('self') 196 | self.data = kwargs_local 197 | response = self._conn.put(urn, self.data) 198 | response["_type"] = "general" 199 | return IuguSubscription(**response) 200 | 201 | def save(self): 202 | """Saves an instance of subscription and return own class instance 203 | modified""" 204 | 205 | if self.id: 206 | sid = self.id 207 | else: 208 | raise errors.IuguSubscriptionsException(value="Save is support "\ 209 | "only to returned API object.") 210 | 211 | kwargs = {} 212 | # TODO: to improve this ineffective approach 213 | # Currently this check if the required set's parameters was passed 214 | # If changes occurs in set() to revise this k in if used to mount kwargs 215 | for k, v in self.__dict__.items(): 216 | if v is not None: 217 | if k == "plan_identifier" or \ 218 | k == "expires_at" or k == "subitems" or \ 219 | k == "suspended" or k == "skip_charge" or \ 220 | k == "custom_variables": 221 | kwargs[k] = v 222 | last_valid_k = k 223 | 224 | if isinstance(v, list) and len(v) == 0 and last_valid_k: 225 | # solves problem with arguments of empty lists 226 | del kwargs[last_valid_k] 227 | 228 | return self.set(sid, **kwargs) 229 | 230 | @classmethod 231 | def get(self, sid): 232 | """ 233 | Fetch one subscription based in ID and returns one of two's types of 234 | subscriptions: credit_based or no credit_based 235 | 236 | :param sid: ID of an existent subscriptions in API 237 | """ 238 | urn = "/v1/subscriptions/{sid}".format(sid=sid) 239 | response = self._conn.get(urn, []) 240 | 241 | if self.is_credit_based(response): 242 | response["_type"] = "credit_based" 243 | return SubscriptionCreditsBased(**response) 244 | 245 | response["_type"] = "general" 246 | return IuguSubscription(**response) 247 | 248 | @classmethod 249 | def getitems(self, limit=None, skip=None, created_at_from=None, 250 | created_at_to=None, query=None, updated_since=None, sort=None, 251 | customer_id=None): 252 | """ 253 | Gets subscriptions by API default limited 100. 254 | """ 255 | data = [] 256 | urn = "/v1/subscriptions/" 257 | 258 | # Set options 259 | if limit: 260 | data.append(("limit", limit)) 261 | if skip: 262 | data.append(("start", skip)) 263 | if created_at_from: 264 | data.append(("created_at_from", created_at_from)) 265 | if created_at_to: 266 | data.append(("created_at_to", created_at_to)) 267 | if updated_since: 268 | data.append(("updated_since", updated_since)) 269 | if query: 270 | data.append(("query", query)) 271 | if customer_id: 272 | data.append(("customer_id", customer_id)) 273 | 274 | # TODO: sort not work fine. Waiting support of API providers 275 | if sort: 276 | assert sort is not str, "sort must be string as -name or name" 277 | 278 | if sort.startswith("-"): 279 | sort = sort[1:] 280 | key = "sortBy[{field}]".format(field=sort) 281 | data.append((key, "desc")) 282 | else: 283 | key = "sortBy[{field}]".format(field=sort) 284 | data.append((key, "asc")) 285 | 286 | subscriptions = self._conn.get(urn, data) 287 | subscriptions_objs = [] 288 | for s in subscriptions["items"]: 289 | # add items in list but before verifies if credit_based 290 | if self.is_credit_based(s): 291 | s["_type"] = "credit_based" 292 | obj_subscription = SubscriptionCreditsBased(**s) 293 | else: 294 | s["_type"] = "general" 295 | obj_subscription = IuguSubscription(**s) 296 | 297 | subscriptions_objs.append(obj_subscription) 298 | 299 | return subscriptions_objs 300 | 301 | def remove(self, sid=None): 302 | """ 303 | Removes a subscription given id or instance 304 | 305 | :param sid: ID of an existent subscriptions in API 306 | """ 307 | if not sid: 308 | if self.id: 309 | sid = self.id 310 | else: 311 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 312 | 313 | urn = "/v1/subscriptions/{sid}".format(sid=sid) 314 | self._conn.delete(urn, []) 315 | 316 | def suspend(self, sid=None): 317 | """ 318 | Suspends an existent subscriptions 319 | 320 | :param sid: ID of an existent subscriptions in API 321 | """ 322 | if not sid: 323 | if self.id: 324 | sid = self.id 325 | else: 326 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 327 | 328 | urn = "/v1/subscriptions/{sid}/suspend".format(sid=sid) 329 | response = self._conn.post(urn, []) 330 | 331 | if self.is_credit_based(response): 332 | response["_type"] = "credit_based" 333 | return SubscriptionCreditsBased(**response) 334 | 335 | response["_type"] = "general" 336 | return IuguSubscription(**response) 337 | 338 | def activate(self, sid=None): 339 | """ 340 | Activates an existent subscriptions 341 | 342 | :param sid: ID of an existent subscriptions in API 343 | 344 | NOTE: This option not work fine by API 345 | """ 346 | if not sid: 347 | if self.id: 348 | sid = self.id 349 | else: 350 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 351 | 352 | urn = "/v1/subscriptions/{sid}/activate".format(sid=sid) 353 | response = self._conn.post(urn, []) 354 | 355 | if self.is_credit_based(response): 356 | response["_type"] = "credit_based" 357 | return SubscriptionCreditsBased(**response) 358 | 359 | response["_type"] = "general" 360 | return IuguSubscription(**response) 361 | 362 | def change_plan(self, plan_identifier, sid=None): 363 | """ 364 | Changes the plan for existent subscriptions 365 | 366 | :param sid: ID of an existent subscriptions in API 367 | :param plan_identifier: the identifier of a plan (it's not ID) 368 | """ 369 | if not sid: 370 | if self.id: 371 | # short-circuit 372 | if "credits_based" in self.__dict__ and self.credits_based: 373 | raise errors.\ 374 | IuguSubscriptionsException(value="Instance must be " \ 375 | "object of IuguSubscriptionsException") 376 | sid = self.id 377 | else: 378 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 379 | 380 | urn = "/v1/subscriptions/{sid}/change_plan/{plan_identifier}"\ 381 | .format(sid=sid, plan_identifier=plan_identifier) 382 | response = self._conn.post(urn, []) 383 | 384 | if self.is_credit_based(response): 385 | response["_type"] = "credit_based" 386 | return SubscriptionCreditsBased(**response) 387 | 388 | response["_type"] = "general" 389 | return IuguSubscription(**response) 390 | 391 | 392 | class SubscriptionCreditsBased(IuguSubscription): 393 | 394 | """ 395 | 396 | This class make additional approaches for subscriptions based in credits. 397 | Addition methods as add_credits and remove_credits. 398 | 399 | :method create: it has parameters different of class extended 400 | :method set: it has parameters different of class extended 401 | 402 | """ 403 | 404 | def __init__(self, **kwargs): 405 | super(SubscriptionCreditsBased, self).__init__(**kwargs) 406 | self.credits_based = True 407 | self.credits_cycle = kwargs.get("credits_cycle") 408 | self.credits_min = kwargs.get("credits_min") 409 | self.credits = kwargs.get("credits") 410 | 411 | def create(self, customer_id, credits_cycle, price_cents=None, 412 | credits_min=None, expires_at=None, only_on_charge_success=None, 413 | subitems=None, custom_variables=None): 414 | """ 415 | Create a subscription based in credits and return the instance 416 | this class. 417 | 418 | :param: custom_variables: a dict {'key': 'value'} 419 | """ 420 | 421 | if price_cents is None or price_cents <= 0: 422 | raise errors.IuguSubscriptionsException(value="price_cents must be " \ 423 | "greater than 0") 424 | 425 | credits_based = self.credits_based 426 | 427 | if custom_variables: 428 | assert isinstance(custom_variables, dict), "Required a dict" 429 | custom_data = self.custom_variables_list(custom_variables) 430 | 431 | kwargs_local = locals().copy() 432 | kwargs_local.pop('self') 433 | urn = "/v1/subscriptions" 434 | self.data = kwargs_local 435 | response = self._conn.post(urn, self.data) 436 | response["_type"] = "credit_based" 437 | return SubscriptionCreditsBased(**response) 438 | 439 | def set(self, sid, expires_at=None, subitems=None, suspended=None, 440 | skip_charge=None, price_cents=None, credits_cycle=None, 441 | credits_min=None, custom_variables=None): 442 | """ 443 | Changes an existent subscription no credit_based 444 | 445 | :param sid: ID of an existent subscriptions in API 446 | """ 447 | urn = "/v1/subscriptions/{sid}".format(sid=sid) 448 | credits_based = self.credits_based 449 | if custom_variables: 450 | assert isinstance(custom_variables, dict), "Required a dict" 451 | custom_data = self.custom_variables_list(custom_variables) 452 | kwargs_local = locals().copy() 453 | kwargs_local.pop('self') 454 | self.data = kwargs_local 455 | response = self._conn.put(urn, self.data) 456 | response["_type"] = "credit_based" 457 | return SubscriptionCreditsBased(**response) 458 | 459 | def save(self): 460 | """ Saves an instance of this class that was persisted or raise error 461 | if no instance. 462 | 463 | NOTE: to use create() or set() for add/change custom_variables 464 | """ 465 | if self.id: 466 | sid = self.id 467 | else: 468 | raise errors.IuguSubscriptionsException(value="Save is support "\ 469 | "only to returned API object.") 470 | 471 | kwargs = {} 472 | # TODO: to improve this ineffective approach. 473 | # Currently this check if the set's parameters was passed. If changes 474 | # occurs in set() to revise this k in if used to mount kwargs 475 | for k, v in self.__dict__.items(): 476 | if v is not None: 477 | if k == "expires_at" or \ 478 | k == "subitems" or k == "suspended" or \ 479 | k == "skip_charge" or k == "price_cents" or \ 480 | k == "credits_cycle" or k == "credits_min" or \ 481 | k == "custom_variables" : 482 | kwargs[k] = v 483 | last_valid_k = k 484 | 485 | if isinstance(v, list) and len(v) == 0 and last_valid_k: 486 | # solves problem with arguments of empty lists 487 | del kwargs[last_valid_k] 488 | del last_valid_k 489 | 490 | return self.set(sid, **kwargs) 491 | 492 | def add_credits(self, quantity, sid=None): 493 | """ 494 | Adds credits in existent subscriptions 495 | 496 | :param sid: ID of an existent subscriptions in API 497 | :param plan_identifier: the identifier of a plan (it's not ID) 498 | """ 499 | data = [] 500 | if not sid: 501 | if self.id: 502 | if not self.credits_based: 503 | raise errors.\ 504 | IuguSubscriptionsException(value="Instance must be " \ 505 | "object of SubscriptionCreditsBased") 506 | sid = self.id 507 | else: 508 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 509 | 510 | urn = "/v1/subscriptions/{sid}/add_credits".format(sid=sid) 511 | data.append(("quantity", quantity)) 512 | response = self._conn.put(urn, data) 513 | 514 | if not self.is_credit_based(response): 515 | raise errors.IuguSubscriptionsException(value="Instance must be " \ 516 | "object of SubscriptionCreditsBased") 517 | 518 | response["_type"] = "credit_based" 519 | return SubscriptionCreditsBased(**response) 520 | 521 | def remove_credits(self, quantity, sid=None): 522 | """ 523 | Suspends an existent subscriptions 524 | 525 | :param sid: ID of an existent subscriptions in API 526 | :param plan_identifier: the identifier of a plan (it's not ID) 527 | """ 528 | data = [] 529 | if not sid: 530 | if self.id: 531 | if not self.credits_based: 532 | raise errors.\ 533 | IuguSubscriptionsException(value="Instance must be " \ 534 | "object of SubscriptionCreditsBased") 535 | sid = self.id 536 | else: 537 | raise errors.IuguSubscriptionsException(value="ID (sid) can't be empty") 538 | 539 | urn = "/v1/subscriptions/{sid}/remove_credits".format(sid=sid) 540 | data.append(("quantity", quantity)) 541 | response = self._conn.put(urn, data) 542 | 543 | if not self.is_credit_based(response): 544 | raise errors.IuguSubscriptionsException(value="Instance must be " \ 545 | "object of SubscriptionCreditsBased") 546 | 547 | response["_type"] = "credit_based" 548 | return SubscriptionCreditsBased(**response) 549 | -------------------------------------------------------------------------------- /lib/iugu/tests.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | __author__ = 'horacioibrahim' 4 | 5 | import unittest, os 6 | import datetime 7 | from time import sleep, ctime, time 8 | from random import randint 9 | from hashlib import md5 10 | from types import StringType 11 | 12 | # python-iugu package modules 13 | import merchant, customers, config, invoices, errors, plans, subscriptions 14 | 15 | 16 | def check_tests_environment(): 17 | """ 18 | For tests is need environment variables to instantiate merchant. Or 19 | Edit tests file to instantiate merchant.IuguMerchant(account_id=YOUR_ID) 20 | """ 21 | try: 22 | global ACCOUNT_ID 23 | ACCOUNT_ID = os.environ["ACCOUNT_ID"] 24 | except KeyError: 25 | raise errors.IuguConfigTestsErrors("Only for tests is required an environment " \ 26 | "variable ACCOUNT_ID or edit file tests.py") 27 | 28 | class TestMerchant(unittest.TestCase): 29 | 30 | check_tests_environment() # Checks if enviroment variables defined 31 | def setUp(self): 32 | self.EMAIL_CUSTOMER = "anyperson@ap.com" 33 | self.client = merchant.IuguMerchant(account_id=ACCOUNT_ID, 34 | api_mode_test=True) 35 | 36 | def tearDown(self): 37 | pass 38 | 39 | def test_create_payment_token_is_test(self): 40 | response = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', 41 | '12', '2010', '123') 42 | self.assertTrue(response.is_test) 43 | 44 | def test_create_payment_token(self): 45 | response = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', 46 | '12', '2010', '123') 47 | self.assertEqual(response.status, 200) 48 | 49 | def test_create_charge_credit_card(self): 50 | item = merchant.Item("Produto My Test", 1, 10000) 51 | token = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', 52 | '12', '2010', '123') 53 | charge = self.client.create_charge(self.EMAIL_CUSTOMER, item, token=token) 54 | self.assertEqual(charge.is_success(), True) 55 | 56 | def test_create_charge_bank_slip(self): 57 | item = merchant.Item("Produto Bank Slip", 1, 1000) 58 | charge = self.client.create_charge(self.EMAIL_CUSTOMER, item) 59 | self.assertEqual(charge.is_success(), True) 60 | 61 | 62 | class TestCustomer(unittest.TestCase): 63 | 64 | def setUp(self): 65 | hash_md5 = md5() 66 | number = randint(1, 50) 67 | hash_md5.update(str(number)) 68 | email = "{email}@test.com".format(email=hash_md5.hexdigest()) 69 | self.random_user_email = email 70 | 71 | 72 | def test_create_customer_basic_info(self): 73 | consumer = customers.IuguCustomer(api_mode_test=True, 74 | email=self.random_user_email) 75 | c = consumer.create() 76 | c.remove() 77 | self.assertEqual(consumer.email, c.email) 78 | 79 | def test_create_customer_basic_email(self): 80 | consumer = customers.IuguCustomer() 81 | c = consumer.create(email=self.random_user_email) 82 | c.remove() 83 | self.assertEqual(consumer.email, c.email) 84 | 85 | def test_create_customer_extra_attrs(self): 86 | consumer = customers.IuguCustomer(api_mode_test=True, 87 | email=self.random_user_email) 88 | c = consumer.create(name="Mario Lago", notes="It's the man", 89 | custom_variables={'local':'cup'}) 90 | c.remove() 91 | self.assertEqual(c.custom_variables[0]['name'], "local") 92 | self.assertEqual(c.custom_variables[0]['value'], "cup") 93 | 94 | def test_get_customer(self): 95 | consumer = customers.IuguCustomer(api_mode_test=True, 96 | email=self.random_user_email) 97 | consumer_new = consumer.create() 98 | c = consumer.get(customer_id=consumer_new.id) 99 | consumer_new.remove() 100 | self.assertEqual(consumer.email, c.email) 101 | 102 | def test_set_customer(self): 103 | consumer = customers.IuguCustomer(api_mode_test=True, 104 | email=self.random_user_email) 105 | consumer_new = consumer.create(name="Mario Lago", notes="It's the man", 106 | custom_variables={'local':'cup'}) 107 | c = consumer.set(consumer_new.id, name="Lago Mario") 108 | consumer_new.remove() 109 | self.assertEqual(c.name, "Lago Mario") 110 | 111 | def test_customer_save(self): 112 | consumer = customers.IuguCustomer(api_mode_test=True, 113 | email=self.random_user_email) 114 | consumer_new = consumer.create(name="Mario Lago", notes="It's the man", 115 | custom_variables={'local':'cup'}) 116 | 117 | # Edit info 118 | consumer_new.name = "Ibrahim Horacio" 119 | # Save as instance 120 | consumer_new.save() 121 | # verify results 122 | check_user = consumer.get(consumer_new.id) 123 | consumer_new.remove() 124 | self.assertEqual(check_user.name, "Ibrahim Horacio") 125 | 126 | def test_customer_delete_by_id(self): 127 | consumer = customers.IuguCustomer(api_mode_test=True, 128 | email=self.random_user_email) 129 | consumer_new = consumer.create(name="Mario Lago", notes="It's the man", 130 | custom_variables={'local':'cup'}) 131 | consumer.delete(consumer_new.id) 132 | self.assertRaises(errors.IuguGeneralException, consumer.get, 133 | consumer_new.id) 134 | 135 | def test_customer_delete_instance(self): 136 | consumer = customers.IuguCustomer(api_mode_test=True, 137 | email=self.random_user_email) 138 | consumer_new = consumer.create(name="Mario Lago", notes="It's the man", 139 | custom_variables={'local':'cup'}) 140 | 141 | r = consumer_new.remove() 142 | self.assertRaises(errors.IuguGeneralException, consumer.get, 143 | consumer_new.id) 144 | 145 | 146 | class TestCustomerLists(unittest.TestCase): 147 | 148 | def setUp(self): 149 | hash_md5 = md5() 150 | number = randint(1, 50) 151 | hash_md5.update(str(number)) 152 | email = "{email}@test.com".format(email=hash_md5.hexdigest()) 153 | self.random_user_email = email 154 | 155 | self.c = customers.IuguCustomer(api_mode_test=True, 156 | email=self.random_user_email) 157 | 158 | # creating customers for tests with lists 159 | p1, p2, p3 = "Andrea", "Bruna", "Carol" 160 | self.one = self.c.create(name=p1, notes="It's the man", 161 | custom_variables={'local':'cup'}) 162 | 163 | # I'm not happy with it (sleep), but was need. This certainly occurs because 164 | # time data is not a timestamp. 165 | sleep(1) 166 | self.two = self.c.create(name=p2, notes="It's the man", 167 | custom_variables={'local':'cup'}) 168 | sleep(1) 169 | self.three = self.c.create(name=p3, notes="It's the man", 170 | custom_variables={'local':'cup'}) 171 | sleep(1) 172 | 173 | self.p1, self.p2, self.p3 = p1, p2, p3 174 | 175 | def tearDown(self): 176 | self.one.remove() 177 | self.two.remove() 178 | self.three.remove() 179 | 180 | def test_getitems(self): 181 | customers_list = self.c.getitems() 182 | self.assertEqual(type(customers_list), list) 183 | 184 | def test_getitems_limit(self): 185 | # get items with auto DESC order 186 | customers_list = self.c.getitems(limit=2) 187 | self.assertEqual(len(customers_list), 2) 188 | 189 | def test_getitems_start(self): 190 | # get items with auto DESC order 191 | sleep(2) 192 | customers_list = self.c.getitems(limit=3) # get latest three customers 193 | reference_customer = customers_list[2].name 194 | customers_list = self.c.getitems(skip=2) 195 | self.assertEqual(customers_list[0].name, reference_customer) 196 | 197 | def test_getitems_query_by_match_in_name(self): 198 | hmd5 = md5() 199 | hmd5.update(ctime(time())) 200 | salt = hmd5.hexdigest() 201 | term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) 202 | 203 | # test value/term in >>name<< 204 | customer = self.c.create(name=term) 205 | sleep(2) 206 | items = self.c.getitems(query=term) # assert valid because name 207 | customer.remove() 208 | self.assertEqual(items[0].name, term) 209 | 210 | def test_getitems_query_by_match_in_notes(self): 211 | hmd5 = md5() 212 | hmd5.update(ctime(time())) 213 | salt = hmd5.hexdigest() 214 | term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) 215 | # test value/term in >>notes<< 216 | customer = self.c.create(name="Sub Zero", notes=term) 217 | sleep(2) 218 | items = self.c.getitems(query=term) 219 | customer.remove() 220 | self.assertEqual(items[0].notes, term) 221 | 222 | def test_getitems_query_by_match_in_email(self): 223 | hmd5 = md5() 224 | hmd5.update(ctime(time())) 225 | salt = hmd5.hexdigest() 226 | term = 'name_inexistent_or_improbable_here_{salt}'.format(salt=salt) 227 | # test value/term in >>email<< 228 | email = term + '@email.com' 229 | self.c.email = email 230 | customer = self.c.create() 231 | sleep(2) 232 | items = self.c.getitems(query=term) 233 | customer.remove() 234 | self.assertIn(term, items[0].email) 235 | 236 | # Uncomment/comment the next one line to disable/enable the test 237 | @unittest.skip("Database of webservice is not empty") 238 | def test_getitems_sort(self): 239 | sleep(1) # Again. It's need 240 | # Useful to test database with empty data (previous data, old tests) 241 | customers_list = self.c.getitems(sort="name") 242 | 243 | # monkey skip 244 | if len(customers_list) < 4: 245 | self.assertEqual(customers_list[0].name, self.p1) 246 | else: 247 | raise TypeError("API Database is not empty. This test isn't useful. " \ 248 | "Use unittest.skip() before this method.") 249 | 250 | def test_getitems_created_at_from(self): 251 | sleep(1) 252 | customers_list = self.c.getitems(created_at_from=self.three.created_at) 253 | self.assertEqual(customers_list[0].id, self.three.id) 254 | 255 | # Uncomment the next one line to disable the test 256 | # @unittest.skip("Real-time interval not reached") 257 | def test_getitems_created_at_to(self): 258 | sleep(1) 259 | customers_list = self.c.getitems(created_at_to=self.one.created_at) 260 | self.assertEqual(customers_list[0].id, self.three.id) 261 | 262 | def test_getitems_updated_since(self): 263 | # get items with auto DESC order 264 | sleep(1) 265 | customers_list = self.c.getitems(updated_since=self.three.created_at) 266 | self.assertEqual(customers_list[0].id, self.three.id) 267 | 268 | 269 | class TestCustomerPayments(unittest.TestCase): 270 | 271 | def setUp(self): 272 | hash_md5 = md5() 273 | number = randint(1, 50) 274 | hash_md5.update(str(number)) 275 | email = "{email}@test.com".format(email=hash_md5.hexdigest()) 276 | self.random_user_email = email 277 | self.client = customers.IuguCustomer(email="test@testmail.com") 278 | self.customer = self.client.create() 279 | 280 | self.instance_payment = self.customer.payment.create(description="New payment method", 281 | number='4111111111111111', 282 | verification_value=123, 283 | first_name="Joao", last_name="Maria", 284 | month=12, year=2014) 285 | 286 | def tearDown(self): 287 | self.instance_payment.remove() 288 | self.customer.remove() # if you remove customer also get payment 289 | 290 | def test_create_payment_method_new_user_by_create(self): 291 | """ Test create payment method to new recent user returned by create() 292 | of IuguCustomer 293 | """ 294 | instance_payment = self.customer.payment.create(description="New payment method", 295 | number='4111111111111111', 296 | verification_value=123, 297 | first_name="Joao", last_name="Maria", 298 | month=12, year=2014) 299 | instance_payment.remove() 300 | self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) 301 | 302 | def test_create_payment_method_existent_user_by_get(self): 303 | """ Test create payment method of existent user returned by get() 304 | of IuguCustomer. 305 | """ 306 | new_customer = self.client.create() 307 | # Test with user from get() 308 | existent_customer = self.client.get(new_customer.id) 309 | 310 | instance_payment = existent_customer.payment.create(description="New payment method", 311 | number='4111111111111111', 312 | verification_value=123, 313 | first_name="Joao", last_name="Maria", 314 | month=12, year=2015) 315 | instance_payment.remove() 316 | self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) 317 | 318 | def test_create_payment_method_existent_user_by_getitems(self): 319 | """ Test create payment method of existent user returned by getitems() 320 | of IuguCustomer 321 | """ 322 | # Test with user from getitems() 323 | customers_list = self.client.getitems() 324 | c_0 = customers_list[0] 325 | 326 | instance_payment = c_0.payment.create(description="New payment method", 327 | number='4111111111111111', 328 | verification_value=123, 329 | first_name="Joao", last_name="Maria", 330 | month=12, year=2016) 331 | instance_payment.remove() 332 | self.assertTrue(isinstance(instance_payment, customers.IuguPaymentMethod)) 333 | 334 | def test_create_payment_method_non_existent_user_by_instance(self): 335 | """ Test create payment method to instance's user before it was 336 | created in API. So without ID. 337 | """ 338 | create = self.client.payment.create 339 | 340 | self.assertRaises(errors.IuguPaymentMethodException, 341 | create, description="New payment method", 342 | number='4111111111111111', 343 | verification_value=123, first_name="Joao", 344 | last_name="Maria", month=12, year=2016) 345 | 346 | def test_create_payment_method_raise_general(self): 347 | # Create payment method without data{} where API returns error. 348 | customer = self.client.create() 349 | self.assertRaises(errors.IuguGeneralException, customer.payment.create, 350 | description="Second payment method") 351 | customer.remove() 352 | 353 | def test_get_payment_method_by_payment_id_customer_id(self): 354 | # Test get payment based payment_id and customer_id 355 | id = self.instance_payment.id 356 | # two args passed 357 | payment = self.client.payment.get(id, customer_id=self.customer.id) 358 | self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) 359 | 360 | def test_get_payment_by_customer(self): 361 | # Test get payment by instance's customer (existent in API) 362 | id = self.instance_payment.id 363 | # one arg passed. user is implicit to customer 364 | payment = self.customer.payment.get(id) 365 | self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) 366 | 367 | def test_set_payment_by_payment_id_customer_id(self): 368 | # Changes payment method base payment_id and customer_id 369 | id = self.instance_payment.id 370 | # two args passed 371 | payment = self.client.payment.set(id, "New Card Name", 372 | customer_id=self.customer.id) 373 | self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) 374 | payment_test = self.customer.payment.get(payment.id) 375 | self.assertEqual(payment_test.description, payment.description) 376 | 377 | def test_set_payment_by_customer(self): 378 | # Changes payment method base payment_id of an intance's customer 379 | id = self.instance_payment.id 380 | # one arg passed. user is implicit to customer 381 | payment = self.customer.payment.set(id, "New Card Name") 382 | self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) 383 | payment_test = self.customer.payment.get(payment.id) 384 | self.assertEqual(payment_test.description, payment.description) 385 | 386 | def test_set_payment_by_customer_by_save(self): 387 | """ Changes payment method of an instance's payment no payment_id or 388 | no customer_id is need""" 389 | self.instance_payment.description = "New Card Name" 390 | # no args passed. To payment method instance this is implicit 391 | payment = self.instance_payment.save() 392 | self.assertTrue(isinstance(payment, customers.IuguPaymentMethod)) 393 | payment_test = self.customer.payment.get(payment.id) 394 | self.assertEqual(payment_test.description, payment.description) 395 | 396 | def test_set_payment_remove(self): 397 | """ Changes payment method of an instance's payment no payment_id or 398 | no customer_id is need""" 399 | instance_payment = self.customer.payment.create(description="New payment method", 400 | number='4111111111111111', 401 | verification_value=123, 402 | first_name="Joao", last_name="Maria", 403 | month=12, year=2014) 404 | instance_payment.remove() 405 | # Try get payment already removed 406 | payment_test = self.customer.payment.get # copy method 407 | self.assertRaises(errors.IuguGeneralException, payment_test, 408 | instance_payment.id) 409 | 410 | def test_set_payment_remove_by_attrs(self): 411 | """ 412 | 413 | """ 414 | instance_payment = self.customer.payment 415 | instance_payment.payment_data.description = "New payment method" 416 | instance_payment.payment_data.number = number='4111111111111111' 417 | instance_payment.payment_data.verification_value = 123 418 | instance_payment.payment_data.first_name = "Joao" 419 | instance_payment.payment_data.last_name = "Silva" 420 | instance_payment.payment_data.month = 12 421 | instance_payment.payment_data.year = 2015 422 | instance_payment = instance_payment.create(description="Meu cartao") 423 | instance_payment.remove() 424 | self.assertRaises(errors.IuguGeneralException, instance_payment.get, instance_payment.id) 425 | 426 | def test_getitems_payments(self): 427 | payment_one = self.customer.payment.create(description="New payment One", 428 | number='4111111111111111', 429 | verification_value=123, 430 | first_name="World", last_name="Cup", 431 | month=12, year=2014) 432 | payment_two = self.customer.payment.create(description="New payment Two", 433 | number='4111111111111111', 434 | verification_value=123, 435 | first_name="Is a ", last_name="Problem", 436 | month=12, year=2015) 437 | payment_three = self.customer.payment.create(description="New payment Three", 438 | number='4111111111111111', 439 | verification_value=123, 440 | first_name="To Brazil", last_name="Worry", 441 | month=12, year=2015) 442 | list_of_payments = self.customer.payment.getitems() 443 | self.assertTrue(isinstance(list_of_payments, list)) 444 | self.assertTrue(isinstance(list_of_payments[0], 445 | customers.IuguPaymentMethod)) 446 | 447 | 448 | class TestInvoice(unittest.TestCase): 449 | TODAY = datetime.date.today().strftime("%d/%m/%Y") 450 | check_tests_environment() # Checks if enviroment variables defined 451 | 452 | def setUp(self): 453 | hash_md5 = md5() 454 | number = randint(1, 50) 455 | hash_md5.update(str(number)) 456 | email = "{email}@test.com".format(email=hash_md5.hexdigest()) 457 | self.customer_email = email 458 | # create a customer for tests 459 | c = customers.IuguCustomer() 460 | self.consumer = c.create(email="client@customer.com") 461 | 462 | # create a invoice 463 | item = merchant.Item("Prod 1", 1, 1190) 464 | self.item = item 465 | self.invoice_obj = invoices.IuguInvoice(email=self.customer_email, 466 | item=item, due_date=self.TODAY) 467 | self.invoice = self.invoice_obj.create(draft=True) 468 | 469 | # to tests for refund 470 | self.EMAIL_CUSTOMER = "anyperson@ap.com" 471 | self.client = merchant.IuguMerchant(account_id=ACCOUNT_ID, 472 | api_mode_test=True) 473 | 474 | def tearDown(self): 475 | if self.invoice.id: # if id is None already was removed 476 | self.invoice.remove() 477 | self.consumer.remove() 478 | 479 | def test_invoice_raise_required_email(self): 480 | i = invoices.IuguInvoice() 481 | self.assertRaises(errors.IuguInvoiceException, i.create, 482 | due_date="30/11/2020", items=self.item) 483 | 484 | def test_invoice_raise_required_due_date(self): 485 | i = invoices.IuguInvoice() 486 | self.assertRaises(errors.IuguInvoiceException, i.create, 487 | email="h@gmail.com", items=self.item) 488 | 489 | def test_invoice_raise_required_items(self): 490 | i = invoices.IuguInvoice() 491 | self.assertRaises(errors.IuguInvoiceException, i.create, 492 | due_date="30/11/2020", email="h@gmail.com") 493 | 494 | def test_invoice_create_basic(self): 495 | self.assertTrue(isinstance(self.invoice, invoices.IuguInvoice)) 496 | 497 | def test_invoice_with_customer_id(self): 498 | res = self.invoice_obj.create(customer_id=self.consumer.id) 499 | self.assertEqual(res.customer_id, self.consumer.id) 500 | 501 | def test_invoice_create_all_fields_as_draft(self): 502 | response = self.invoice_obj.create(draft=True, return_url='http://hipy.co/success', 503 | expired_url='http://hipy.co/expired', 504 | notification_url='http://hipy.co/webhooks', 505 | tax_cents=200, discount_cents=500, 506 | customer_id=self.consumer.id, 507 | ignore_due_email=True) 508 | self.assertTrue(isinstance(response, invoices.IuguInvoice)) 509 | existent_invoice = invoices.IuguInvoice.get(response.id) 510 | self.assertEqual(existent_invoice.expiration_url, response.expiration_url) 511 | response.remove() 512 | 513 | def test_invoice_create_all_fields_as_pending(self): 514 | response = self.invoice_obj.create(draft=False, 515 | return_url='http://example.com/success', 516 | expired_url='http://example.com/expired', 517 | notification_url='http://example.com/webhooks', 518 | tax_cents=200, discount_cents=500, 519 | customer_id=self.consumer.id, 520 | ignore_due_email=True) 521 | self.assertTrue(isinstance(response, invoices.IuguInvoice)) 522 | # The comments below was put because API don't exclude 523 | # an invoice that was paid. So only does refund. 524 | # response.remove() 525 | 526 | def test_invoice_created_check_id(self): 527 | self.assertIsNotNone(self.invoice.id) 528 | 529 | def test_invoice_create_with_custom_variables_in_create(self): 530 | invoice = self.invoice_obj.create(draft=True, 531 | custom_variables={'city': 'Brasilia'}) 532 | self.assertEqual(invoice.custom_variables[0]["name"], "city") 533 | self.assertEqual(invoice.custom_variables[0]["value"], "Brasilia") 534 | invoice.remove() 535 | 536 | def test_invoice_create_with_custom_variables_in_set(self): 537 | invoice = self.invoice_obj.set(invoice_id=self.invoice.id, 538 | custom_variables={'city': 'Brasilia'}) 539 | self.assertEqual(invoice.custom_variables[0]["name"], "city") 540 | self.assertEqual(invoice.custom_variables[0]["value"], "Brasilia") 541 | 542 | def test_invoice_get_one(self): 543 | # test start here 544 | res = invoices.IuguInvoice.get(self.invoice.id) 545 | self.assertEqual(res.items[0].description, "Prod 1") 546 | 547 | def test_invoice_create_as_draft(self): 548 | self.assertEqual(self.invoice.status, 'draft') 549 | 550 | def test_invoice_edit_email_with_set(self): 551 | id = self.invoice.id 552 | invoice_edited = self.invoice_obj.set(invoice_id=id, email="now@now.com") 553 | self.assertEqual(invoice_edited.email, u"now@now.com") 554 | 555 | def test_invoice_edit_return_url_with_set(self): 556 | return_url = "http://hipy.co" 557 | id = self.invoice.id 558 | invoice_edited = self.invoice_obj.set(invoice_id=id, 559 | return_url=return_url) 560 | self.assertEqual(invoice_edited.return_url, return_url) 561 | 562 | @unittest.skip("It isn't support by API") 563 | def test_invoice_edit_expired_url_with_set(self): 564 | expired_url = "http://hipy.co" 565 | id = self.invoice.id 566 | invoice_edited = self.invoice_obj.set(invoice_id=id, 567 | expired_url=expired_url) 568 | self.assertEqual(invoice_edited.expiration_url, expired_url) 569 | 570 | def test_invoice_edit_notification_url_with_set(self): 571 | notification_url = "http://hipy.co" 572 | id = self.invoice.id 573 | invoice_edited = self.invoice_obj.set(invoice_id=id, 574 | notification_url=notification_url) 575 | self.assertEqual(invoice_edited.notification_url, notification_url) 576 | 577 | def test_invoice_edit_tax_cents_with_set(self): 578 | tax_cents = 200 579 | id = self.invoice.id 580 | invoice_edited = self.invoice_obj.set(invoice_id=id, 581 | tax_cents=tax_cents) 582 | self.assertEqual(invoice_edited.tax_cents, tax_cents) 583 | 584 | def test_invoice_edit_discount_cents_with_set(self): 585 | discount_cents = 500 586 | id = self.invoice.id 587 | invoice_edited = self.invoice_obj.set(invoice_id=id, 588 | discount_cents=discount_cents) 589 | self.assertEqual(invoice_edited.discount_cents, discount_cents) 590 | 591 | def test_invoice_edit_customer_id_with_set(self): 592 | customer_id = self.consumer.id 593 | id = self.invoice.id 594 | invoice_edited = self.invoice_obj.set(invoice_id=id, 595 | customer_id=customer_id) 596 | self.assertEqual(invoice_edited.customer_id, customer_id) 597 | 598 | @unittest.skip("without return from API of the field/attribute ignore_due_email") 599 | def test_invoice_edit_ignore_due_email_with_set(self): 600 | ignore_due_email = True 601 | id = self.invoice.id 602 | invoice_edited = self.invoice_obj.set(invoice_id=id, 603 | ignore_due_email=ignore_due_email) 604 | self.assertEqual(invoice_edited.ignore_due_email, ignore_due_email) 605 | 606 | # TODO: def test_invoice_edit_subscription_id_with_set(self): 607 | 608 | # TODO: test_invoice_edit_credits_with_set(self): 609 | 610 | def test_invoice_edit_due_date_with_set(self): 611 | due_date = self.TODAY 612 | response_from_api = str(datetime.date.today()) 613 | id = self.invoice.id 614 | invoice_edited = self.invoice_obj.set(invoice_id=id, 615 | due_date=due_date) 616 | self.assertEqual(invoice_edited.due_date, response_from_api) 617 | 618 | def test_invoice_edit_items_with_set(self): 619 | self.invoice.items[0].description = "Prod Fixed Text and Value" 620 | id = self.invoice.id 621 | items = self.invoice.items[0] 622 | invoice_edited = self.invoice_obj.set(invoice_id=id, items=items) 623 | self.assertEqual(invoice_edited.items[0].description, "Prod Fixed Text and Value") 624 | 625 | def test_invoice_changed_items_with_save(self): 626 | self.invoice.items[0].description = "Prod Saved by Instance" 627 | # inv_one is instance not saved. Now, we have invoice saved 628 | # and invoice_edited that is the response of webservice 629 | res = self.invoice.save() 630 | self.assertEqual(res.items[0].description, "Prod Saved by Instance") 631 | 632 | def test_invoice_destroy_item(self): 633 | # Removes one item, the unique, created in invoice 634 | self.invoice.items[0].remove() 635 | re_invoice = self.invoice.save() 636 | self.assertEqual(re_invoice.items, None) 637 | 638 | def test_invoice_remove(self): 639 | # wait webservice response time 640 | sleep(3) 641 | self.invoice.remove() 642 | self.assertEqual(self.invoice.id, None) 643 | 644 | def test_invoice_get_and_save(self): 645 | inv = invoices.IuguInvoice.get(self.invoice.id) 646 | inv.email = "test_save@save.com" 647 | obj = inv.save() 648 | self.assertEqual(obj.email, inv.email) 649 | 650 | def test_invoice_getitems_and_save(self): 651 | sleep(2) # wating...API to persist data 652 | inv = None 653 | invs = invoices.IuguInvoice.getitems() 654 | for i in invs: 655 | if i.id == self.invoice.id: 656 | inv = i 657 | inv.email = "test_save@save.com" 658 | obj = inv.save() 659 | self.assertEqual(obj.email, inv.email) 660 | 661 | def test_invoice_cancel(self): 662 | invoice = self.invoice_obj.create(draft=False) 663 | re_invoice = invoice.cancel() 664 | self.assertEqual(re_invoice.status, "canceled") 665 | invoice.remove() 666 | 667 | #@unittest.skip("Support only invoice paid") # TODO 668 | def test_invoice_refund(self): 669 | item = merchant.Item("Produto My Test", 1, 10000) 670 | token = self.client.create_payment_token('4111111111111111', 'JA', 'Silva', 671 | '12', '2010', '123') 672 | charge = self.client.create_charge(self.EMAIL_CUSTOMER, item, token=token) 673 | invoice = invoices.IuguInvoice.get(charge.invoice_id) 674 | re_invoice = invoice.refund() 675 | self.assertEqual(re_invoice.status, "refunded") 676 | 677 | def test_invoice_getitems(self): 678 | # wait webservice response time 679 | sleep(3) 680 | l = invoices.IuguInvoice.getitems() 681 | self.assertIsInstance(l, list) 682 | self.assertIsInstance(l[0], invoices.IuguInvoice) 683 | 684 | def test_invoice_getitems_limit(self): 685 | invoice_2 = self.invoice_obj.create() 686 | sleep(3) 687 | l = invoices.IuguInvoice.getitems(limit=2) 688 | # The comments below was put because API don't exclude 689 | # an invoice that was paid. So only does refund. 690 | # invoice_2.remove() 691 | self.assertEqual(len(l), 2) 692 | 693 | def test_invoice_getitems_skip(self): 694 | invoice_1 = self.invoice_obj.create() 695 | invoice_2 = self.invoice_obj.create() 696 | invoice_3 = self.invoice_obj.create() 697 | sleep(3) 698 | l1 = invoices.IuguInvoice.getitems(limit=3) 699 | keep_checker = l1[2] 700 | l2 = invoices.IuguInvoice.getitems(skip=2) 701 | skipped = l2[0] # after skip 2 the first must be keep_checker 702 | # The comments below was put because API don't exclude 703 | # an invoice that was paid. So only does refund. 704 | # invoice_1.remove() 705 | # invoice_2.remove() 706 | # invoice_3.remove() 707 | self.assertEqual(keep_checker.id, skipped.id) 708 | 709 | # TODO: def test_invoice_getitems_created_at_from(self): 710 | 711 | # TODO:def test_invoice_getitems_created_at_to(self): 712 | 713 | # TODO: def test_invoice_getitems_updated_since(self): 714 | 715 | def test_invoice_getitems_query(self): 716 | res = self.invoice_obj.create(customer_id=self.consumer.id) 717 | sleep(3) 718 | queryset = invoices.IuguInvoice.getitems(query=res.id) 719 | self.assertEqual(queryset[0].customer_id, res.customer_id) 720 | # The comments below was put because API don't exclude 721 | # an invoice that was paid. So only does refund. 722 | # res.remove() 723 | 724 | def test_invoice_getitems_customer_id(self): 725 | res = self.invoice_obj.create(customer_id=self.consumer.id) 726 | sleep(3) 727 | queryset = invoices.IuguInvoice.getitems(query=res.id) 728 | self.assertEqual(queryset[0].customer_id, res.customer_id) 729 | # The comments below was put because API don't exclude 730 | # an invoice that was paid. So only does refund. 731 | # res.remove() 732 | 733 | @unittest.skip("API no support sort (in moment)") 734 | def test_invoice_getitems_sort(self): 735 | invoice_1 = self.invoice_obj.create() 736 | invoice_2 = self.invoice_obj.create() 737 | invoice_3 = self.invoice_obj.create() 738 | sleep(3) 739 | l1 = invoices.IuguInvoice.getitems(limit=3) 740 | keep_checker = l1[2] 741 | l2 = invoices.IuguInvoice.getitems(limit=3, sort="id") 742 | skipped = l2[0] # after skip 2 the first must be keep_checker 743 | # The comments below was put because API don't exclude 744 | # an invoice that was paid. So only does refund. 745 | # invoice_1.remove() 746 | # invoice_2.remove() 747 | # invoice_3.remove() 748 | self.assertEqual(keep_checker.id, skipped.id) 749 | 750 | 751 | class TestPlans(unittest.TestCase): 752 | 753 | def setUp(self): 754 | hash_md5 = md5() 755 | seed = randint(1, 199) 756 | variation = randint(4, 8) 757 | hash_md5.update(str(seed)) 758 | identifier = hash_md5.hexdigest()[:variation] 759 | self.identifier = identifier # random because can't be repeated 760 | plan = plans.IuguPlan() 761 | self.plan = plan.create(name="My SetUp Plan", identifier=self.identifier, 762 | interval=1, interval_type="months", 763 | currency="BRL", value_cents=1500) 764 | 765 | # features 766 | self.features = plans.Feature() 767 | self.features.name = "Add feature %s" % self.identifier 768 | self.features.identifier = self.identifier 769 | self.features.value = 11 770 | 771 | def tearDown(self): 772 | self.plan.remove() 773 | 774 | def test_plan_create(self): 775 | plan = plans.IuguPlan() 776 | identifier = self.identifier + "salt" 777 | new_plan = plan.create(name="My first lib Plan", identifier=identifier, 778 | interval=1, interval_type="months", 779 | currency="BRL", value_cents=1000) 780 | self.assertIsInstance(new_plan, plans.IuguPlan) 781 | self.assertTrue(new_plan.id) 782 | new_plan.remove() 783 | 784 | def test_plan_create_without_required_fields(self): 785 | plan = plans.IuguPlan() 786 | self.assertRaises(errors.IuguPlansException, plan.create) 787 | 788 | def test_plan_create_features(self): 789 | salt = randint(1, 99) 790 | identifier = self.identifier + str(salt) 791 | # init object 792 | plan = plans.IuguPlan(name="Plan with features", identifier=identifier, 793 | interval=1, interval_type="months", 794 | currency="BRL", value_cents=1000) 795 | 796 | plan.features = [self.features,] 797 | new_plan_with_features = plan.create() 798 | self.assertIsInstance(new_plan_with_features.features[0], plans.Feature) 799 | self.assertEqual(new_plan_with_features.features[0].value, self.features.value) 800 | new_plan_with_features.remove() 801 | 802 | def test_plan_get(self): 803 | plan_id = self.plan.id 804 | plan = plans.IuguPlan.get(plan_id) 805 | self.assertEqual(self.identifier, plan.identifier) 806 | 807 | def test_plan_get_identifier(self): 808 | plan = plans.IuguPlan.get_by_identifier(self.identifier) 809 | self.assertEqual(self.identifier, plan.identifier) 810 | 811 | def test_plan_remove(self): 812 | plan = plans.IuguPlan() 813 | new_plan = plan.create(name="Remove me", identifier="to_remove", 814 | interval=1, interval_type="months", 815 | currency="BRL", value_cents=2000) 816 | removed_id = new_plan.id 817 | new_plan.remove() 818 | self.assertRaises(errors.IuguGeneralException, 819 | plans.IuguPlan.get, removed_id) 820 | 821 | def test_plan_edit_changes_name_by_set(self): 822 | plan_id = self.plan.id 823 | new_name = "New name %s" % self.identifier 824 | modified_plan = self.plan.set(plan_id, name=new_name) 825 | self.assertEqual(new_name, modified_plan.name) 826 | 827 | def test_plan_edit_changes_identifier_by_set(self): 828 | plan_id = self.plan.id 829 | new_identifier = "New identifier %s" % self.identifier 830 | modified_plan = self.plan.set(plan_id, identifier=new_identifier) 831 | self.assertEqual(new_identifier, modified_plan.identifier) 832 | 833 | def test_plan_edit_changes_interval_by_set(self): 834 | plan_id = self.plan.id 835 | new_interval = 3 836 | modified_plan = self.plan.set(plan_id, interval=new_interval) 837 | self.assertEqual(new_interval, modified_plan.interval) 838 | 839 | def test_plan_edit_changes_currency_by_set(self): 840 | plan_id = self.plan.id 841 | new_currency = "US" 842 | self.assertRaises(errors.IuguPlansException, self.plan.set, 843 | plan_id, currency=new_currency) 844 | 845 | def test_plan_edit_changes_value_cents_by_set(self): 846 | plan_id = self.plan.id 847 | value_cents = 3000 848 | modified_plan = self.plan.set(plan_id, value_cents=value_cents) 849 | self.assertEqual(value_cents, modified_plan.prices[0].value_cents) 850 | 851 | def test_plan_edit_changes_features_name_by_set(self): 852 | salt = randint(1, 99) 853 | identifier = self.identifier + str(salt) 854 | 855 | # creating a plan with features 856 | plan = plans.IuguPlan() 857 | plan.features = [self.features,] 858 | plan.name = "Changes Features Name" 859 | plan.identifier = identifier # workaround: setUp already creates 860 | plan.interval = 2 861 | plan.interval_type = "weeks" 862 | plan.currency = "BRL" 863 | plan.value_cents = 3000 864 | plan_returned = plan.create() 865 | 866 | # to change features name where features already has an id 867 | changed_features = plan_returned.features 868 | changed_features[0].name = "Changed Name of Features" 869 | 870 | # return plan changed 871 | plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) 872 | 873 | self.assertEqual(plan_changed.features[0].name, 874 | plan_returned.features[0].name) 875 | plan_returned.remove() 876 | 877 | def test_plan_edit_changes_features_identifier_by_set(self): 878 | salt = randint(1, 99) 879 | identifier = self.identifier + str(salt) 880 | 881 | # creating a plan with features 882 | plan = plans.IuguPlan() 883 | plan.features = [self.features,] 884 | plan.name = "Changes Features Identifier" 885 | plan.identifier = identifier # workaround: setUp already creates 886 | plan.interval = 2 887 | plan.interval_type = "weeks" 888 | plan.currency = "BRL" 889 | plan.value_cents = 3000 890 | plan_returned = plan.create() 891 | 892 | # to change features name where features already has an id 893 | changed_features = plan_returned.features 894 | changed_features[0].identifier = "Crazy_Change" 895 | 896 | # return plan changed 897 | plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) 898 | 899 | self.assertEqual(plan_changed.features[0].identifier, 900 | plan_returned.features[0].identifier) 901 | plan_returned.remove() 902 | 903 | def test_plan_edit_changes_features_value_by_set(self): 904 | salt = randint(1, 99) 905 | identifier = self.identifier + str(salt) 906 | 907 | # creating a plan with features 908 | plan = plans.IuguPlan() 909 | plan.features = [self.features,] 910 | plan.name = "Changes Features Identifier" 911 | plan.identifier = identifier # workaround: setUp already creates 912 | plan.interval = 2 913 | plan.interval_type = "weeks" 914 | plan.currency = "BRL" 915 | plan.value_cents = 3000 916 | plan_returned = plan.create() 917 | 918 | # to change features name where features already has an id 919 | changed_features = plan_returned.features 920 | changed_features[0].value = 10000 921 | 922 | # return plan changed 923 | plan_changed = plan.set(plan_returned.id, features=[changed_features[0]]) 924 | 925 | self.assertEqual(plan_changed.features[0].value, 926 | plan_returned.features[0].value) 927 | plan_returned.remove() 928 | 929 | def test_plan_edit_changes_name_by_save(self): 930 | self.plan.name = "New name %s" % self.identifier 931 | response = self.plan.save() 932 | self.assertEqual(response.name, self.plan.name) 933 | 934 | def test_plan_edit_changes_identifier_by_save(self): 935 | seed = randint(1, 999) 936 | self.plan.identifier = "New_identifier_%s_%s" % (self.identifier, 937 | seed) 938 | response = self.plan.save() 939 | self.assertEqual(response.identifier, self.plan.identifier) 940 | 941 | def test_plan_edit_changes_interval_by_save(self): 942 | self.plan.interval = 4 943 | response = self.plan.save() 944 | self.assertEqual(response.interval, 4) 945 | 946 | def test_plan_edit_changes_currency_by_save(self): 947 | # API only support BRL 948 | self.plan.currency = "US" 949 | # response = self.plan.save() 950 | self.assertRaises(errors.IuguPlansException, self.plan.save) 951 | 952 | def test_plan_edit_changes_value_cents_by_save(self): 953 | self.plan.value_cents = 4000 954 | response = self.plan.save() 955 | self.assertEqual(response.prices[0].value_cents, 4000) 956 | 957 | # TODO: test prices attribute of plan in level one 958 | 959 | def test_plan_edit_changes_features_name_by_save(self): 960 | salt = randint(1, 99) 961 | identifier = self.identifier + str(salt) 962 | 963 | # creating a plan with features 964 | plan = plans.IuguPlan() 965 | plan.features = [self.features,] 966 | plan.name = "Changes Features by Save" 967 | plan.identifier = identifier # workaround: setUp already creates 968 | plan.interval = 2 969 | plan.interval_type = "weeks" 970 | plan.currency = "BRL" 971 | plan.value_cents = 3000 972 | plan_returned = plan.create() 973 | 974 | # to change features name where features already has an id 975 | to_change_features = plan_returned.features 976 | to_change_features[0].name = "Features New by Save" 977 | 978 | # return plan changed and to save instance 979 | plan_returned.features = [to_change_features[0]] 980 | plan_changed = plan_returned.save() 981 | 982 | self.assertEqual(plan_changed.features[0].name, "Features New by Save") 983 | plan_returned.remove() 984 | 985 | def test_plan_edit_changes_features_identifier_by_save(self): 986 | salt = randint(1, 99) 987 | identifier = self.identifier + str(salt) 988 | 989 | # creating a plan with features 990 | plan = plans.IuguPlan() 991 | plan.features = [self.features,] 992 | plan.name = "Changes Features by Save" 993 | plan.identifier = identifier # workaround: setUp already creates 994 | plan.interval = 2 995 | plan.interval_type = "weeks" 996 | plan.currency = "BRL" 997 | plan.value_cents = 3000 998 | plan_returned = plan.create() 999 | 1000 | # to change features name where features already has an id 1001 | to_change_features = plan_returned.features 1002 | to_change_features[0].identifier = "Crazy_Changed" 1003 | 1004 | # return plan changed and to save instance 1005 | plan_returned.features = [to_change_features[0]] 1006 | plan_changed = plan_returned.save() 1007 | 1008 | self.assertEqual(plan_changed.features[0].identifier, "Crazy_Changed") 1009 | plan_returned.remove() 1010 | 1011 | def test_plan_edit_changes_features_value_by_save(self): 1012 | salt = randint(1, 99) 1013 | identifier = self.identifier + str(salt) 1014 | 1015 | # creating a plan with features 1016 | plan = plans.IuguPlan() 1017 | plan.features = [self.features,] 1018 | plan.name = "Changes Features by Save" 1019 | plan.identifier = identifier # workaround: setUp already creates 1020 | plan.interval = 2 1021 | plan.interval_type = "weeks" 1022 | plan.currency = "BRL" 1023 | plan.value_cents = 3000 1024 | plan_returned = plan.create() 1025 | 1026 | # to change features name where features already has an id 1027 | to_change_features = plan_returned.features 1028 | to_change_features[0].value = 8000 1029 | 1030 | # return plan changed and to save instance 1031 | plan_returned.features = [to_change_features[0]] 1032 | plan_changed = plan_returned.save() 1033 | 1034 | self.assertEqual(plan_changed.features[0].value, 8000) 1035 | plan_returned.remove() 1036 | 1037 | def test_plan_getitems_filter_limit(self): 1038 | # creating a plan with features 1039 | salt = str(randint(1, 199)) + self.identifier 1040 | plan = plans.IuguPlan() 1041 | plan_a = plan.create(name="Get Items...", 1042 | identifier=salt, interval=2, 1043 | interval_type="weeks", currency="BRL", 1044 | value_cents=1000) 1045 | salt = str(randint(1, 199)) + self.identifier 1046 | plan_b = plan.create(name="Get Items...", 1047 | identifier=salt, interval=2, 1048 | interval_type="weeks", currency="BRL", 1049 | value_cents=2000) 1050 | salt = str(randint(1, 199)) + self.identifier 1051 | plan_c = plan.create(name="Get Items...", 1052 | identifier=salt, interval=2, 1053 | interval_type="weeks", currency="BRL", 1054 | value_cents=3000) 1055 | 1056 | all_plans = plans.IuguPlan.getitems(limit=3) 1057 | self.assertEqual(len(all_plans), 3) 1058 | plan_a.remove() 1059 | plan_b.remove() 1060 | plan_c.remove() 1061 | 1062 | def test_plan_getitems_filter_skip(self): 1063 | # creating a plan with features 1064 | salt = str(randint(1, 199)) + self.identifier 1065 | plan = plans.IuguPlan() 1066 | plan_a = plan.create(name="Get Items...", 1067 | identifier=salt, interval=2, 1068 | interval_type="weeks", currency="BRL", 1069 | value_cents=1000) 1070 | salt = str(randint(1, 199)) + self.identifier 1071 | plan_b = plan.create(name="Get Items...", 1072 | identifier=salt, interval=2, 1073 | interval_type="weeks", currency="BRL", 1074 | value_cents=2000) 1075 | salt = str(randint(1, 199)) + self.identifier 1076 | plan_c = plan.create(name="Get Items...", 1077 | identifier=salt, interval=2, 1078 | interval_type="weeks", currency="BRL", 1079 | value_cents=3000) 1080 | 1081 | sleep(2) 1082 | all_plans_limit = plans.IuguPlan.getitems(limit=3) 1083 | all_plans_skip = plans.IuguPlan.getitems(skip=2, limit=3) 1084 | self.assertEqual(all_plans_limit[2].id, all_plans_skip[0].id) 1085 | plan_a.remove() 1086 | plan_b.remove() 1087 | plan_c.remove() 1088 | 1089 | def test_plan_getitems_filter_query(self): 1090 | salt = str(randint(1, 199)) + self.identifier 1091 | name_repeated = salt 1092 | plan = plans.IuguPlan() 1093 | plan_a = plan.create(name=name_repeated, 1094 | identifier=salt, interval=2, 1095 | interval_type="weeks", currency="BRL", 1096 | value_cents=1000) 1097 | salt = str(randint(1, 199)) + self.identifier 1098 | plan_b = plan.create(name=name_repeated, 1099 | identifier=salt, interval=2, 1100 | interval_type="weeks", currency="BRL", 1101 | value_cents=2000) 1102 | salt = str(randint(1, 199)) + self.identifier 1103 | plan_c = plan.create(name=name_repeated, 1104 | identifier=salt, interval=2, 1105 | interval_type="weeks", currency="BRL", 1106 | value_cents=3000) 1107 | sleep(3) # waiting API to keep data 1108 | all_filter_query = plans.IuguPlan.getitems(query=name_repeated) 1109 | self.assertEqual(all_filter_query[0].name, name_repeated) 1110 | self.assertEqual(len(all_filter_query), 3) 1111 | plan_a.remove() 1112 | plan_b.remove() 1113 | plan_c.remove() 1114 | 1115 | #@unittest.skip("TODO support this test") 1116 | # TODO: def test_plan_getitems_filter_updated_since(self): 1117 | 1118 | #@unittest.skip("Sort not work fine. Waiting support of API providers") 1119 | #def test_plan_getitems_filter_sort(self): 1120 | 1121 | 1122 | class TestSubscriptions(unittest.TestCase): 1123 | 1124 | 1125 | def clean_invoices(self, recent_invoices): 1126 | """ 1127 | Removes invoices created in backgrounds of tests 1128 | """ 1129 | # The comments below was put because API don't exclude 1130 | # an invoice that was paid. So only does refund. (API CHANGED) 1131 | # 1132 | #if recent_invoices: 1133 | # invoice = recent_invoices[0] 1134 | # invoices.IuguInvoice().remove(invoice_id=invoice["id"]) 1135 | pass 1136 | 1137 | def setUp(self): 1138 | # preparing object... 1139 | seed = randint(1, 10000) 1140 | md5_hash = md5() 1141 | md5_hash.update(str(seed)) 1142 | plan_id_random = md5_hash.hexdigest()[:12] 1143 | plan_name = "Subs Plan %s" % plan_id_random 1144 | name = "Ze %s" % plan_id_random 1145 | email = "{name}@example.com".format(name=plan_id_random) 1146 | # plans for multiple tests 1147 | self.plan_new = plans.IuguPlan().create(name=plan_name, 1148 | identifier=plan_id_random, 1149 | interval=1, 1150 | interval_type="weeks", 1151 | currency="BRL", 1152 | value_cents=9900) 1153 | 1154 | plan_identifier = "plan_for_changes_%s" % plan_id_random 1155 | self.plan_two = plans.IuguPlan().create(name="Plan Two", 1156 | identifier=plan_identifier, interval=1, 1157 | interval_type="weeks", currency="BRL", 1158 | value_cents=8800) 1159 | # one client 1160 | self.customer = customers.IuguCustomer().create(name=name, email=email) 1161 | # for tests to edit subscriptions 1162 | subs_obj = subscriptions.IuguSubscription() 1163 | self.subscription = subs_obj.create(customer_id=self.customer.id, 1164 | plan_identifier=self.plan_two.identifier) 1165 | 1166 | def tearDown(self): 1167 | # Attempt to delete the invoices created by subscriptions cases 1168 | # But this not remove all invoices due not recognizable behavior 1169 | # as the API no forever return recents_invoices for created 1170 | # invoices 1171 | 1172 | # The comments below was put because API don't exclude 1173 | # an invoice that was paid. So only does refund. (API CHANGED) 1174 | #### if self.subscription.recent_invoices: 1175 | #### invoice = self.subscription.recent_invoices[0] 1176 | #### # to instanciate invoice from list of the invoices returned by API 1177 | #### invoice_obj = invoices.IuguInvoice.get(invoice["id"]) 1178 | #### # The comments below was put because API don't exclude 1179 | #### # an invoice that was paid. So only does refund. 1180 | #### invoice_obj.remove() 1181 | self.plan_new.remove() 1182 | self.plan_two.remove() 1183 | self.subscription.remove() 1184 | self.customer.remove() 1185 | 1186 | def test_subscription_create(self): 1187 | # Test to create a subscription only client_id and plan_identifier 1188 | p_obj = subscriptions.IuguSubscription() 1189 | subscription_new = p_obj.create(self.customer.id, self.plan_new.identifier) 1190 | self.assertIsInstance(subscription_new, subscriptions.IuguSubscription) 1191 | self.assertEqual(subscription_new.plan_identifier, self.plan_new.identifier) 1192 | self.clean_invoices(subscription_new.recent_invoices) 1193 | subscription_new.remove() 1194 | 1195 | def test_subscription_create_with_custom_variables(self): 1196 | p_obj = subscriptions.IuguSubscription() 1197 | subscription_new = p_obj.create(self.customer.id, 1198 | self.plan_new.identifier, 1199 | custom_variables={'city':'Recife'}) 1200 | self.assertEqual(subscription_new.custom_variables[0]["name"], "city") 1201 | self.assertEqual(subscription_new.custom_variables[0]["value"], "Recife") 1202 | self.clean_invoices(subscription_new.recent_invoices) 1203 | subscription_new.remove() 1204 | 1205 | def test_subscription_set_with_custom_variables(self): 1206 | p_obj = subscriptions.IuguSubscription() 1207 | subscription_new = p_obj.set(sid=self.subscription.id, 1208 | custom_variables={'city':'Recife'}) 1209 | self.assertEqual(subscription_new.custom_variables[0]["name"], "city") 1210 | self.assertEqual(subscription_new.custom_variables[0]["value"], "Recife") 1211 | # self.clean_invoices(subscription_new.recent_invoices) 1212 | 1213 | @unittest.skip("API does not support this only_on_charge_success. CHANGED") 1214 | def test_subscription_create_only_on_charge_success_with_payment(self): 1215 | # Test to create subscriptions with charge only 1216 | customer = customers.IuguCustomer().create(name="Pay now", 1217 | email="pay_now@local.com") 1218 | pay = customer.payment.create(description="Payment X", 1219 | number="4111111111111111", 1220 | verification_value='123', 1221 | first_name="Romario", last_name="Baixo", 1222 | month=12, year=2018) 1223 | p_obj = subscriptions.IuguSubscription() 1224 | new_subscription = p_obj.create(customer.id, self.plan_new.identifier, 1225 | only_on_charge_success=True) 1226 | self.assertEqual(new_subscription.recent_invoices[0]["status"], "paid") 1227 | self.clean_invoices(new_subscription.recent_invoices) 1228 | new_subscription.remove() 1229 | customer.remove() 1230 | 1231 | def test_subscription_create_only_on_charge_success_less_payment(self): 1232 | # Test to create subscriptions with charge only 1233 | p_obj = subscriptions.IuguSubscription() 1234 | self.assertRaises(errors.IuguGeneralException, p_obj.create, 1235 | self.customer.id, self.plan_new.identifier, 1236 | only_on_charge_success=True) 1237 | 1238 | def test_subscription_remove(self): 1239 | # Test to remove subscription 1240 | p_obj = subscriptions.IuguSubscription() 1241 | subscription_new = p_obj.create(self.customer.id, self.plan_new.identifier) 1242 | sid = subscription_new.id 1243 | self.clean_invoices(subscription_new.recent_invoices) 1244 | subscription_new.remove() 1245 | self.assertRaises(errors.IuguGeneralException, 1246 | subscriptions.IuguSubscription.get, sid) 1247 | 1248 | def test_subscription_get(self): 1249 | subscription = subscriptions.IuguSubscription.get(self.subscription.id) 1250 | self.assertIsInstance(subscription, subscriptions.IuguSubscription) 1251 | 1252 | def test_subscription_getitems(self): 1253 | subscription_list = subscriptions.IuguSubscription.getitems() 1254 | self.assertIsInstance(subscription_list[0], subscriptions.IuguSubscription) 1255 | 1256 | def test_subscription_getitem_limit(self): 1257 | client_subscriptions = subscriptions.IuguSubscription() 1258 | sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1259 | sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1260 | sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1261 | sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1262 | sleep(3) # slower API 1263 | subscriptions_list = subscriptions.IuguSubscription.getitems(limit=1) 1264 | self.assertEqual(len(subscriptions_list), 1) 1265 | self.assertEqual(subscriptions_list[0].id, sub_4.id) 1266 | self.clean_invoices(sub_1.recent_invoices) 1267 | self.clean_invoices(sub_2.recent_invoices) 1268 | self.clean_invoices(sub_3.recent_invoices) 1269 | self.clean_invoices(sub_4.recent_invoices) 1270 | a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() 1271 | 1272 | def test_subscription_getitem_skip(self): 1273 | client_subscriptions = subscriptions.IuguSubscription() 1274 | sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1275 | sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1276 | sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1277 | sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1278 | sleep(2) 1279 | subscriptions_list = subscriptions.IuguSubscription.getitems(skip=1) 1280 | self.assertEqual(subscriptions_list[0].id, sub_3.id) 1281 | self.clean_invoices(sub_1.recent_invoices) 1282 | self.clean_invoices(sub_2.recent_invoices) 1283 | self.clean_invoices(sub_3.recent_invoices) 1284 | self.clean_invoices(sub_4.recent_invoices) 1285 | a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() 1286 | 1287 | # TODO: def test_subscription_getitem_created_at_from(self): 1288 | 1289 | def test_subscription_getitem_query(self): 1290 | term = self.customer.name 1291 | sleep(3) # very slow API! waiting... 1292 | subscriptions_list = subscriptions.IuguSubscription.getitems(query=term) 1293 | self.assertGreaterEqual(len(subscriptions_list), 1) 1294 | 1295 | # TODO: def test_subscription_getitem_updated_since(self): 1296 | 1297 | @unittest.skip("API not support this. No orders is changed") 1298 | def test_subscription_getitem_sort(self): 1299 | client_subscriptions = subscriptions.IuguSubscription() 1300 | sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1301 | sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1302 | sub_3 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1303 | sub_4 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1304 | subscriptions_list = subscriptions.IuguSubscription.getitems(sort="-created_at") 1305 | #self.assertEqual(subscriptions_list[0].id, sub_3.id) 1306 | self.clean_invoices(sub_1.recent_invoices) 1307 | self.clean_invoices(sub_2.recent_invoices) 1308 | self.clean_invoices(sub_3.recent_invoices) 1309 | self.clean_invoices(sub_4.recent_invoices) 1310 | a, b, c, d = sub_1.remove(), sub_2.remove(), sub_3.remove(), sub_4.remove() 1311 | 1312 | def test_subscription_getitem_customer_id(self): 1313 | 1314 | client_subscriptions = subscriptions.IuguSubscription() 1315 | # previous subscription was created in setUp 1316 | sub_1 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1317 | sub_2 = client_subscriptions.create(self.customer.id, self.plan_new.identifier) 1318 | sleep(3) 1319 | subscriptions_list = subscriptions.IuguSubscription.\ 1320 | getitems(customer_id=self.customer.id) 1321 | self.assertEqual(len(subscriptions_list), 3) # sub_1 + sub_2 + setUp 1322 | self.clean_invoices(sub_1.recent_invoices) 1323 | self.clean_invoices(sub_2.recent_invoices) 1324 | a, b = sub_1.remove(), sub_2.remove() 1325 | 1326 | def test_subscription_set_plan(self): 1327 | # Test to change an existent plan in subscription 1328 | subs = subscriptions.IuguSubscription() 1329 | subscription = subs.create(self.customer.id, self.plan_new.identifier) 1330 | sid = subscription.id 1331 | plan_identifier = self.plan_new.identifier + str("_Newest_ID") 1332 | # changes to this new plan 1333 | plan_newest = plans.IuguPlan().create("Plan Name: Newest", 1334 | plan_identifier, 1, "months", "BRL", 5000) 1335 | # editing... 1336 | subscription = subscriptions.IuguSubscription().set(sid, 1337 | plan_identifier=plan_newest.identifier) 1338 | self.assertEqual(subscription.plan_identifier, plan_identifier) 1339 | self.clean_invoices(subscription.recent_invoices) 1340 | subscription.remove() 1341 | plan_newest.remove() 1342 | 1343 | @unittest.skip("API does not support. It returns error 'Subscription Not Found'") 1344 | def test_subscription_set_customer_id(self): 1345 | # Test if customer_id changed. Iugu's support (number 782) 1346 | customer = customers.IuguCustomer().create(name="Cortella", 1347 | email="mcortella@usp.br") 1348 | subscription = subscriptions.IuguSubscription().\ 1349 | set(self.subscription.id, customer_id=customer.id) 1350 | 1351 | self.assertEqual(subscription.customer_id, customer.id) 1352 | customer.remove() 1353 | 1354 | def test_subscription_set_expires_at(self): 1355 | # Test if expires_at was changed 1356 | subscription = subscriptions.IuguSubscription().\ 1357 | set(self.subscription.id, expires_at="12/12/2014") 1358 | self.assertEqual(subscription.expires_at, "2014-12-12") 1359 | 1360 | def test_subscription_set_suspended(self): 1361 | # Test if suspended was changed 1362 | subscription = subscriptions.IuguSubscription().\ 1363 | set(self.subscription.id, suspended=True) 1364 | self.assertEqual(subscription.suspended, True) 1365 | 1366 | @unittest.skip("Waiting API developers to support this question") 1367 | def test_subscription_set_skip_charge(self): 1368 | # Test if skip_charge was marked 1369 | print self.subscription.id 1370 | subscription = subscriptions.IuguSubscription().\ 1371 | set(self.subscription.id, skip_charge=True) 1372 | self.assertEqual(subscription.suspended, True) 1373 | 1374 | def test_subscription_set_subitems(self): 1375 | # Test if to insert a new item 1376 | subitem = merchant.Item("Subitems", 1, 2345) 1377 | subscription = subscriptions.IuguSubscription().\ 1378 | set(self.subscription.id, subitems=[subitem,]) 1379 | self.assertEqual(subscription.subitems[0].description, 1380 | subitem.description) 1381 | 1382 | def test_subscription_set_subitems_description(self): 1383 | # Test if subitem/item descriptions was changed 1384 | subitem = merchant.Item("Subitems", 1, 2345) 1385 | subscription = subscriptions.IuguSubscription().\ 1386 | set(self.subscription.id, subitems=[subitem,]) 1387 | item_with_id = subscription.subitems[0] 1388 | item_with_id.description = "Subitems Edited" 1389 | subscription = subscriptions.IuguSubscription().\ 1390 | set(self.subscription.id, subitems=[item_with_id,] ) 1391 | self.assertEqual(subscription.subitems[0].description, 1392 | item_with_id.description) 1393 | 1394 | def test_subscription_set_subitems_price_cents(self): 1395 | # Test if subitem/item price_cents was changed 1396 | subitem = merchant.Item("Subitems", 1, 2345) 1397 | subscription = subscriptions.IuguSubscription().\ 1398 | set(self.subscription.id, subitems=[subitem,]) 1399 | item_with_id = subscription.subitems[0] 1400 | item_with_id.price_cents = 2900 1401 | subscription = subscriptions.IuguSubscription().\ 1402 | set(self.subscription.id, subitems=[item_with_id,] ) 1403 | self.assertEqual(subscription.subitems[0].price_cents, 1404 | item_with_id.price_cents) 1405 | 1406 | def test_subscription_set_subitems_quantity(self): 1407 | # Test if subitem/item quantity was changed 1408 | subitem = merchant.Item("Subitems", 1, 2345) 1409 | subscription = subscriptions.IuguSubscription().\ 1410 | set(self.subscription.id, subitems=[subitem,]) 1411 | item_with_id = subscription.subitems[0] 1412 | item_with_id.quantity = 4 1413 | subscription = subscriptions.IuguSubscription().\ 1414 | set(self.subscription.id, subitems=[item_with_id,] ) 1415 | self.assertEqual(subscription.subitems[0].quantity, 1416 | item_with_id.quantity) 1417 | 1418 | def test_subscription_set_subitems_recurrent(self): 1419 | # Test if subitem/item recurrent was changed 1420 | subitem = merchant.Item("Subitems", 1, 2345) 1421 | subscription = subscriptions.IuguSubscription().\ 1422 | set(self.subscription.id, subitems=[subitem,]) 1423 | item_with_id = subscription.subitems[0] 1424 | item_with_id.recurrent = True 1425 | subscription = subscriptions.IuguSubscription().\ 1426 | set(self.subscription.id, subitems=[item_with_id,]) 1427 | self.assertEqual(subscription.subitems[0].recurrent, 1428 | item_with_id.recurrent) 1429 | 1430 | def test_subscription_set_subitems_destroy(self): 1431 | # Test if subitem/item was erased 1432 | subitem = merchant.Item("Subitems", 1, 2345) 1433 | subscription = subscriptions.IuguSubscription().\ 1434 | set(self.subscription.id, subitems=[subitem,]) 1435 | item_with_id = subscription.subitems[0] 1436 | item_with_id.destroy = True 1437 | subscription = subscriptions.IuguSubscription().\ 1438 | set(self.subscription.id, subitems=[item_with_id,]) 1439 | self.assertEqual(subscription.subitems, []) 1440 | 1441 | def test_subscription_create_credit_based_with_custom_variables(self): 1442 | # Test if price_cents changed 1443 | subscription = subscriptions.SubscriptionCreditsBased().\ 1444 | create(self.customer.id, credits_cycle=2, price_cents=10, 1445 | custom_variables={'city':"Recife"}) 1446 | self.assertEqual(subscription.custom_variables[0]['name'], "city") 1447 | self.assertEqual(subscription.custom_variables[0]['value'], "Recife") 1448 | self.clean_invoices(subscription.recent_invoices) 1449 | subscription.remove() 1450 | 1451 | def test_subscription_set_credit_based_with_custom_variables(self): 1452 | # Test if price_cents changed 1453 | subscription = subscriptions.SubscriptionCreditsBased().\ 1454 | create(self.customer.id, credits_cycle=2, price_cents=10) 1455 | subscription = subscriptions.SubscriptionCreditsBased().\ 1456 | set(subscription.id, custom_variables={'city':"Madrid"}) 1457 | self.assertEqual(subscription.custom_variables[0]['name'], "city") 1458 | self.assertEqual(subscription.custom_variables[0]['value'], "Madrid") 1459 | self.clean_invoices(subscription.recent_invoices) 1460 | subscription.remove() 1461 | 1462 | def test_subscription_create_credit_based(self): 1463 | # Test if price_cents changed 1464 | subscription = subscriptions.SubscriptionCreditsBased().\ 1465 | create(self.customer.id, credits_cycle=2, price_cents=10) 1466 | 1467 | self.assertIsInstance(subscription, subscriptions.SubscriptionCreditsBased) 1468 | self.clean_invoices(subscription.recent_invoices) 1469 | subscription.remove() 1470 | 1471 | def test_subscription_create_credit_based_error_price_cents(self): 1472 | # Test if price_cents changed 1473 | subscription = subscriptions.SubscriptionCreditsBased() 1474 | self.assertRaises(errors.IuguSubscriptionsException, 1475 | subscription.create, self.customer.id, 1476 | credits_cycle=2, price_cents=0) 1477 | 1478 | def test_subscription_create_credit_based_error_price_cents_empty(self): 1479 | # Test if price_cents changed 1480 | subscription = subscriptions.SubscriptionCreditsBased() 1481 | self.assertRaises(errors.IuguSubscriptionsException, 1482 | subscription.create, self.customer.id, 1483 | credits_cycle=2, price_cents=None) 1484 | 1485 | def test_subscription_create_credit_based_price_cents(self): 1486 | # Test if price_cents changed 1487 | subscription = subscriptions.SubscriptionCreditsBased().\ 1488 | create(self.customer.id, credits_cycle=2, price_cents=2000) 1489 | 1490 | self.assertEqual(subscription.price_cents, 2000) 1491 | self.clean_invoices(subscription.recent_invoices) 1492 | subscription.remove() 1493 | 1494 | def test_subscription_create_credit_based_credits_cycle(self): 1495 | # Test if price_cents changed 1496 | subscription = subscriptions.SubscriptionCreditsBased().\ 1497 | create(self.customer.id, credits_cycle=2, price_cents=2000) 1498 | 1499 | self.assertEqual(subscription.credits_cycle, 2) 1500 | self.clean_invoices(subscription.recent_invoices) 1501 | subscription.remove() 1502 | 1503 | def test_subscription_create_credit_based_credits_min(self): 1504 | # Test if price_cents changed 1505 | subscription = subscriptions.SubscriptionCreditsBased().\ 1506 | create(self.customer.id, credits_cycle=2, price_cents=2000, 1507 | credits_min=4000) 1508 | 1509 | self.assertEqual(subscription.credits_min, 4000) 1510 | self.clean_invoices(subscription.recent_invoices) 1511 | subscription.remove() 1512 | 1513 | def test_subscription_set_credit_based_price_cents(self): 1514 | # Test if price_cents changed 1515 | subscription = subscriptions.SubscriptionCreditsBased().\ 1516 | create(self.customer.id, credits_cycle=2, price_cents=1200) 1517 | 1518 | subscription = subscriptions.SubscriptionCreditsBased().\ 1519 | set(subscription.id, price_cents=3249) 1520 | 1521 | self.assertEqual(subscription.price_cents, 3249) 1522 | self.clean_invoices(subscription.recent_invoices) 1523 | subscription.remove() 1524 | 1525 | def test_subscription_set_credits_cycle(self): 1526 | # Test if credits_cycle changed 1527 | subscription = subscriptions.SubscriptionCreditsBased().\ 1528 | create(self.customer.id, credits_cycle=2, price_cents=1300) 1529 | 1530 | subscription = subscriptions.SubscriptionCreditsBased().\ 1531 | set(subscription.id, credits_cycle=10) 1532 | 1533 | self.assertEqual(subscription.credits_cycle, 10) 1534 | self.clean_invoices(subscription.recent_invoices) 1535 | subscription.remove() 1536 | 1537 | def test_subscription_set_credits_min(self): 1538 | # Test if credits_min changed 1539 | subscription = subscriptions.SubscriptionCreditsBased().\ 1540 | create(self.customer.id, credits_cycle=2, price_cents=1400) 1541 | 1542 | subscription = subscriptions.SubscriptionCreditsBased().\ 1543 | set(subscription.id, credits_min=2000) 1544 | 1545 | self.assertEqual(subscription.credits_min, 2000) 1546 | self.clean_invoices(subscription.recent_invoices) 1547 | subscription.remove() 1548 | 1549 | def test_subscription_credit_based_get(self): 1550 | # Test if credits_min changed 1551 | subscription = subscriptions.SubscriptionCreditsBased().\ 1552 | create(self.customer.id, credits_cycle=2, price_cents=2000) 1553 | 1554 | subscription = subscriptions.SubscriptionCreditsBased().\ 1555 | get(subscription.id) 1556 | 1557 | self.assertIsInstance(subscription, subscriptions.SubscriptionCreditsBased) 1558 | self.clean_invoices(subscription.recent_invoices) 1559 | subscription.remove() 1560 | 1561 | def test_subscription_credit_based_getitems(self): 1562 | # Test if credits_min changed 1563 | subscription = subscriptions.SubscriptionCreditsBased().\ 1564 | create(self.customer.id, credits_cycle=2, price_cents=2000) 1565 | 1566 | sleep(2) 1567 | subscription_list = subscriptions.SubscriptionCreditsBased().\ 1568 | getitems() 1569 | 1570 | self.assertIsInstance(subscription_list[0], subscriptions.SubscriptionCreditsBased) 1571 | self.clean_invoices(subscription.recent_invoices) 1572 | subscription.remove() 1573 | 1574 | # Test save method 1575 | 1576 | @unittest.skip("This is not support by API. Return not found") 1577 | def test_subscription_save_customer_id(self): 1578 | # Iugu's support (number 782) 1579 | customer = customers.IuguCustomer().create(name="Subs save", 1580 | email="subs_save@local.com") 1581 | self.subscription.customer_id = customer.id 1582 | obj = self.subscription.save() 1583 | self.assertEqual(customer.id, obj.customer_id) 1584 | customer.remove() 1585 | 1586 | def test_subscription_save_expires_at(self): 1587 | self.subscription.expires_at = "12/12/2020" 1588 | obj = self.subscription.save() 1589 | self.assertEqual(obj.expires_at, "2020-12-12") 1590 | 1591 | def test_subscription_save_subitems(self): 1592 | # Test if to save a new item 1593 | subitem = merchant.Item("Subitems", 1, 2345) 1594 | self.subscription.subitems = [subitem,] 1595 | obj = self.subscription.save() 1596 | self.assertEqual(obj.subitems[0].description, 1597 | subitem.description) 1598 | 1599 | def test_subscription_save_subitems_description(self): 1600 | # Test if subitem/item descriptions was changed 1601 | subitem = merchant.Item("Subitems", 1, 2345) 1602 | self.subscription.subitems = [subitem,] 1603 | new_subscription = self.subscription.save() 1604 | item_with_id = new_subscription.subitems[0] 1605 | item_with_id.description = "Subitems Edited" 1606 | self.subscription.subitems = [item_with_id] 1607 | obj = self.subscription.save() 1608 | self.assertEqual(obj.subitems[0].description, 1609 | item_with_id.description) 1610 | 1611 | def test_subscription_save_subitems_price_cents(self): 1612 | # Test if subitem/item price_cents was changed 1613 | subitem = merchant.Item("Subitems", 1, 2345) 1614 | self.subscription.subitems = [subitem,] 1615 | new_subscription = self.subscription.save() 1616 | item_with_id = new_subscription.subitems[0] 1617 | item_with_id.price_cents = 2900 1618 | self.subscription.subitems = [item_with_id,] 1619 | obj = self.subscription.save() 1620 | self.assertEqual(obj.subitems[0].price_cents, 1621 | item_with_id.price_cents) 1622 | 1623 | def test_subscription_save_subitems_quantity(self): 1624 | # Test if subitem/item quantity was changed 1625 | subitem = merchant.Item("Subitems", 1, 2345) 1626 | self.subscription.subitems = [subitem,] 1627 | new_subscription = self.subscription.save() 1628 | item_with_id = new_subscription.subitems[0] 1629 | item_with_id.quantity = 4 1630 | self.subscription.subitems = [item_with_id,] 1631 | obj = self.subscription.save() 1632 | self.assertEqual(obj.subitems[0].quantity, 1633 | item_with_id.quantity) 1634 | 1635 | def test_subscription_save_subitems_recurrent(self): 1636 | # Test if subitem/item recurrent was changed 1637 | subitem = merchant.Item("Subitems", 1, 2345) 1638 | self.subscription.subitems = [subitem,] 1639 | new_subscription = self.subscription.save() 1640 | item_with_id = new_subscription.subitems[0] 1641 | item_with_id.recurrent = True 1642 | self.subscription.subitems = [item_with_id,] 1643 | obj = self.subscription.save() 1644 | self.assertEqual(obj.subitems[0].recurrent, 1645 | item_with_id.recurrent) 1646 | 1647 | def test_subscription_save_subitems__destroy(self): 1648 | # Test if subitem/item was erased 1649 | subitem = merchant.Item("Subitems", 1, 2345) 1650 | self.subscription.subitems = [subitem,] 1651 | new_subscription = self.subscription.save() 1652 | item_with_id = new_subscription.subitems[0] 1653 | item_with_id.destroy = True 1654 | self.subscription.subitems = [item_with_id,] 1655 | obj = self.subscription.save() 1656 | self.assertEqual(obj.subitems, []) 1657 | 1658 | def test_subscription_save_suspended(self): 1659 | self.subscription.suspended = True 1660 | obj = self.subscription.save() 1661 | self.assertEqual(obj.suspended, True) 1662 | 1663 | # @unittest.skip("Waiting API developers to support this question") 1664 | # TODO: def test_subscription_save_skip_charge(self): 1665 | 1666 | def test_subscription_save_price_cents(self): 1667 | subscription = subscriptions.SubscriptionCreditsBased() 1668 | subscription = subscription.create(customer_id=self.customer.id, 1669 | credits_cycle=2, price_cents=1000) 1670 | subscription.price_cents = 8188 1671 | obj = subscription.save() 1672 | self.assertEqual(obj.price_cents, 8188) 1673 | self.clean_invoices(subscription.recent_invoices) 1674 | subscription.remove() 1675 | 1676 | def test_subscription_save_credits_cycle(self): 1677 | subscription = subscriptions.SubscriptionCreditsBased() 1678 | subscription = subscription.create(customer_id=self.customer.id, 1679 | credits_cycle=2, price_cents=1000) 1680 | subscription.credits_cycle = 5 1681 | obj = subscription.save() 1682 | self.assertEqual(obj.credits_cycle, 5) 1683 | self.clean_invoices(subscription.recent_invoices) 1684 | subscription.remove() 1685 | 1686 | def test_subscription_save_credits_min(self): 1687 | subscription = subscriptions.SubscriptionCreditsBased() 1688 | subscription = subscription.create(customer_id=self.customer.id, 1689 | credits_cycle=2, price_cents=1100) 1690 | subscription.credits_min = 9000 1691 | obj = subscription.save() 1692 | self.assertEqual(obj.credits_min, 9000) 1693 | self.clean_invoices(subscription.recent_invoices) 1694 | subscription.remove() 1695 | 1696 | def test_subscription_suspend(self): 1697 | obj = subscriptions.IuguSubscription().suspend(self.subscription.id) 1698 | self.assertEqual(obj.suspended, True) 1699 | 1700 | @unittest.skip("API not support this activate by REST .../activate") 1701 | def test_subscription_activate(self): 1702 | obj = subscriptions.IuguSubscription().suspend(self.subscription.id) 1703 | self.subscription.suspended = True 1704 | self.subscription.save() 1705 | obj = subscriptions.IuguSubscription().activate(self.subscription.id) 1706 | self.assertEqual(obj.suspended, False) 1707 | 1708 | def test_subscription_change_plan(self): 1709 | seed = randint(1, 999) 1710 | identifier = "%s_%s" % (self.plan_new.identifier, str(seed)) 1711 | plan_again_change = plans.IuguPlan().create(name="Change Test", 1712 | identifier=identifier, 1713 | interval=1, interval_type="months", 1714 | currency="BRL", value_cents=1111) 1715 | obj = subscriptions.IuguSubscription().change_plan( 1716 | plan_again_change.identifier, 1717 | sid=self.subscription.id) 1718 | self.assertEqual(obj.plan_identifier, identifier) 1719 | self.clean_invoices(obj.recent_invoices) 1720 | plan_again_change.remove() 1721 | 1722 | def test_subscription_change_plan_by_instance(self): 1723 | seed = randint(1, 999) 1724 | identifier = "%s_%s" % (self.plan_new.identifier, str(seed)) 1725 | plan_again_change = plans.IuguPlan().create(name="Change Test", 1726 | identifier=identifier, 1727 | interval=1, interval_type="months", 1728 | currency="BRL", value_cents=1112) 1729 | obj = self.subscription.change_plan(plan_again_change.identifier) 1730 | self.assertEqual(obj.plan_identifier, identifier) 1731 | self.clean_invoices(obj.recent_invoices) 1732 | plan_again_change.remove() 1733 | 1734 | def test_subscription_add_credits(self): 1735 | subscription = subscriptions.SubscriptionCreditsBased() 1736 | subscription = subscription.create(customer_id=self.customer.id, 1737 | credits_cycle=2, price_cents=1100) 1738 | obj = subscriptions.SubscriptionCreditsBased().add_credits(sid=subscription.id, 1739 | quantity=20) 1740 | self.assertEqual(obj.credits, 20) 1741 | self.clean_invoices(subscription.recent_invoices) 1742 | subscription.remove() 1743 | 1744 | def test_subscription_add_credits_by_instance(self): 1745 | subscription = subscriptions.SubscriptionCreditsBased() 1746 | subscription = subscription.create(customer_id=self.customer.id, 1747 | credits_cycle=2, price_cents=1100) 1748 | obj = subscription.add_credits(sid=subscription.id, 1749 | quantity=20) 1750 | self.assertEqual(obj.credits, 20) 1751 | self.clean_invoices(subscription.recent_invoices) 1752 | subscription.remove() 1753 | 1754 | def test_subscription_remove_credits(self): 1755 | subscription = subscriptions.SubscriptionCreditsBased() 1756 | subscription = subscription.create(customer_id=self.customer.id, 1757 | credits_cycle=2, price_cents=1100) 1758 | 1759 | subscription.add_credits(quantity=20) 1760 | obj = subscriptions.SubscriptionCreditsBased().\ 1761 | remove_credits(sid=subscription.id, quantity=5) 1762 | self.assertEqual(obj.credits, 15) 1763 | self.clean_invoices(subscription.recent_invoices) 1764 | subscription.remove() 1765 | 1766 | def test_subscription_remove_credits_by_instance(self): 1767 | subscription = subscriptions.SubscriptionCreditsBased() 1768 | subscription = subscription.create(customer_id=self.customer.id, 1769 | credits_cycle=2, price_cents=1100) 1770 | 1771 | subscription.add_credits(quantity=20) 1772 | sleep(2) 1773 | obj = subscription.remove_credits(quantity=5) 1774 | self.assertEqual(obj.credits, 15) 1775 | self.clean_invoices(subscription.recent_invoices) 1776 | subscription.remove() 1777 | 1778 | 1779 | class TestTransfer(unittest.TestCase): 1780 | # TODO: to create this tests 1781 | pass 1782 | 1783 | 1784 | if __name__ == '__main__': 1785 | unittest.main() 1786 | -------------------------------------------------------------------------------- /lib/iugu/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Useful for developers purposes 3 | """ 4 | 5 | import config, subscriptions, customers, invoices, plans 6 | 7 | if config.API_MODE_TEST is not True: 8 | raise TypeError("::DANGER:: You isn't in mode test. Check your config file") 9 | 10 | def loop_remove(objs_list, verbose=0): 11 | """ Removes all objects of a list returned by getitems() 12 | """ 13 | for i in objs_list: 14 | try: 15 | i.remove() 16 | except: 17 | if verbose >= 2: 18 | print "Not removed due an exception: ", i.id 19 | else: 20 | pass 21 | 22 | def try_remove(client, verbose=0): 23 | objs = client.getitems() 24 | loop_remove(objs, verbose=verbose) 25 | objs = client.getitems() 26 | return len(objs) 27 | 28 | def try_remove_subs(verbose=0): 29 | subs = subscriptions.IuguSubscription 30 | total = try_remove(subs, verbose=verbose) 31 | if verbose >= 1: 32 | print "Total of Subscriptions: ", total 33 | return total 34 | 35 | def try_remove_client(verbose=0): 36 | cvs = customers.IuguCustomer 37 | total = try_remove(cvs, verbose=verbose) 38 | if verbose >= 1: 39 | print "Total of Customers: ", total 40 | return total 41 | 42 | def try_remove_invoices(verbose=0): 43 | ivs = invoices.IuguInvoice 44 | total = try_remove(ivs, verbose=verbose) 45 | if verbose >= 1: 46 | print "Total of Invoices: ", total 47 | return total 48 | 49 | def try_remove_plans(verbose=0): 50 | pls = plans.IuguPlan 51 | total = try_remove(pls, verbose=verbose) 52 | if verbose >= 1: 53 | print "Total of Plans: ", total 54 | return total 55 | 56 | def reset_all(tolerance=100, verbose=1): 57 | """ 58 | Tries to remove all data 59 | 60 | :param tolerance: it's the number of items not deleted because of API's errors 61 | 62 | IMPORTANT: a tolerance very little (e.g. 10) can to cause a loop 63 | """ 64 | # TODO function ... 65 | tolerance_limit = tolerance + (tolerance * .20) 66 | operations_to_remove = [try_remove_plans, try_remove_subs, 67 | try_remove_client, try_remove_invoices] 68 | def _loop_(operation): 69 | r = tolerance * 2 70 | while r > tolerance: 71 | r = operation(verbose=verbose) 72 | if r > tolerance_limit: 73 | break 74 | 75 | for op in operations_to_remove: 76 | _loop_(op) 77 | 78 | 79 | -------------------------------------------------------------------------------- /lib/iugu/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.6" 2 | 3 | #------------------------------------------------------------------------------ 4 | # CHANGELOG: 5 | # ... oldest changes 6 | # 2014-07-07 v0.9.3 - Removed requirement of config.py variables (token, account) 7 | # 2015-02-22 v0.9.4 - Supported changes from API 8 | # 2015-02-22 v0.9.5-1+ - Supported changes from API 9 | -------------------------------------------------------------------------------- /requirement-dev.txt: -------------------------------------------------------------------------------- 1 | coverage==3.7.1 2 | nose==1.3.3 3 | python-dateutil==2.2 4 | six==1.6.1 5 | wsgiref==0.1.2 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from distutils.core import setup 4 | except ImportError: 5 | from setuptools import setup 6 | 7 | import sys, os 8 | from setuptools import find_packages 9 | #sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib')) 10 | #from iugu.version import __version__ 11 | 12 | setup( 13 | name='iugu-python', 14 | version= '0.9.6', 15 | author='Horacio Ibrahim', 16 | author_email='horacioibrahim@gmail.com', 17 | packages=find_packages('lib'), 18 | package_dir={'': 'lib'}, 19 | scripts=[], 20 | url='https://github.com/horacioibrahim/iugu-python', 21 | download_url='https://github.com/horacioibrahim/iugu-python/tarball/master', 22 | license='Apache License', 23 | description='This package is an idiomatic python lib to work with Iugu service', 24 | long_description=""" 25 | This iugu-python lib is the more pythonic way to work with webservices of payments 26 | iugu.com. This provides python objects to each entity of the service as Subscriptions, 27 | Plans, Customers, Invoices, etc. 28 | http://iugu.com/referencias/api - API Reference 29 | """, 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Intended Audience :: Developers', 33 | 'Natural Language :: English', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.6', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Topic :: Software Development :: Libraries :: Python Modules' 40 | ], 41 | keywords=['iugu', 'rest', 'payment'] 42 | ) 43 | --------------------------------------------------------------------------------