├── .coveragerc ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── examples ├── README.md └── flask │ ├── Dockerfile │ ├── README.md │ ├── config.py │ ├── docker-compose.yml │ ├── flask_seguro │ ├── __init__.py │ ├── cart.py │ ├── controllers │ │ ├── __init__.py │ │ └── main │ │ │ ├── __init__.py │ │ │ └── views.py │ ├── products.py │ ├── static │ │ └── css │ │ │ └── app.css │ └── templates │ │ ├── base.jinja2 │ │ ├── cart.jinja2 │ │ ├── checkout.jinja2 │ │ ├── menu.jinja2 │ │ └── products.jinja2 │ ├── requirements.txt │ ├── run.py │ ├── screenshots │ ├── screen1.png │ ├── screen2.png │ └── screen3.png │ └── tests.py ├── pagseguro ├── __init__.py ├── config.py ├── exceptions.py ├── parsers.py └── utils.py ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_config.py ├── test_pagseguro.py ├── test_pagseguro_sandbox.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pagseguro 3 | 4 | [report] 5 | omit = 6 | */python?.?/* 7 | */site-packages/nose/* 8 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: BGhwPmG7ZldZbCRXR9GadtdNC8o7ud4X3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .cache 38 | 39 | # Created by https://www.gitignore.io/api/macos 40 | 41 | ### macOS ### 42 | # General 43 | .DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Icon must end with two \r 48 | Icon 49 | 50 | # Thumbnails 51 | ._* 52 | 53 | # Files that might appear in the root of a volume 54 | .DocumentRevisions-V100 55 | .fseventsd 56 | .Spotlight-V100 57 | .TemporaryItems 58 | .Trashes 59 | .VolumeIcon.icns 60 | .com.apple.timemachine.donotpresent 61 | 62 | # Directories potentially created on remote AFP share 63 | .AppleDB 64 | .AppleDesktop 65 | Network Trash Folder 66 | Temporary Items 67 | .apdisk 68 | 69 | 70 | # End of https://www.gitignore.io/api/macos 71 | 72 | .pytest_cache/* 73 | .vscode/ 74 | ======= 75 | # User-specific stuff 76 | .idea/**/workspace.xml 77 | .idea/**/tasks.xml 78 | .idea/**/usage.statistics.xml 79 | .idea/**/dictionaries 80 | .idea/**/shelf 81 | 82 | # Sensitive or high-churn files 83 | .idea/**/dataSources/ 84 | .idea/**/dataSources.ids 85 | .idea/**/dataSources.local.xml 86 | .idea/**/sqlDataSources.xml 87 | .idea/**/dynamic.xml 88 | .idea/**/uiDesigner.xml 89 | .idea/**/dbnavigator.xml 90 | 91 | # Gradle 92 | .idea/**/gradle.xml 93 | .idea/**/libraries 94 | 95 | # Gradle and Maven with auto-import 96 | # When using Gradle or Maven with auto-import, you should exclude module files, 97 | # since they will be recreated, and may cause churn. Uncomment if using 98 | # auto-import. 99 | # .idea/modules.xml 100 | # .idea/*.iml 101 | # .idea/modules 102 | 103 | # CMake 104 | cmake-build-*/ 105 | 106 | # Mongo Explorer plugin 107 | .idea/**/mongoSettings.xml 108 | 109 | # File-based project format 110 | *.iws 111 | 112 | # IntelliJ 113 | out/ 114 | 115 | # mpeltonen/sbt-idea plugin 116 | .idea_modules/ 117 | 118 | # JIRA plugin 119 | atlassian-ide-plugin.xml 120 | 121 | # Cursive Clojure plugin 122 | .idea/replstate.xml 123 | 124 | # Crashlytics plugin (for Android Studio and IntelliJ) 125 | com_crashlytics_export_strings.xml 126 | crashlytics.properties 127 | crashlytics-build.properties 128 | fabric.properties 129 | 130 | # Editor-based Rest Client 131 | .idea/httpRequests 132 | 133 | .idea/ 134 | 135 | *.iml 136 | modules.xml 137 | .idea/misc.xml 138 | *.ipr 139 | 140 | ### VisualStudioCode ### 141 | .vscode/ 142 | !.vscode/settings.json 143 | !.vscode/tasks.json 144 | !.vscode/launch.json 145 | !.vscode/extensions.json 146 | venv/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | before_install: 8 | - "pip install coveralls" 9 | - "pip install -r requirements_dev.txt" 10 | install: 11 | - "python setup.py install" 12 | script: 13 | - "make test" 14 | after_success: 15 | - coveralls 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Bruno Rocha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | recursive-include tests * 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: pep8 3 | py.test --cov-report term-missing --cov pagseguro 4 | 5 | .PHONY: pep8 6 | pep8: 7 | @flake8 * --ignore=F403,F401 --exclude=requirements.txt,requirements_dev.txt,*.pyc,*.md,Makefile,LICENSE,*.in 8 | 9 | .PHONY: publish 10 | publish: test 11 | @python setup.py sdist upload 12 | 13 | .PHONY: clean-pyc clean-build clean 14 | clean: clean-build clean-pyc 15 | 16 | clean-build: 17 | rm -fr build/ 18 | rm -fr dist/ 19 | rm -fr *.egg-info 20 | rm -fr *.egg 21 | 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | find . -name '__pycache__' -exec rm -fr {} + 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Este projeto agora é mantido pelo @PagSeguro, o repositório foi transferido para o user @Japle que é o responsável pelas libs e integrações no PagSeguro**. -- @rochacbruno 2 | 3 | python-pagseguro 4 | ================ 5 | [![Build 6 | Status](https://travis-ci.org/rochacbruno/python-pagseguro.png)](https://travis-ci.org/rochacbruno/python-pagseguro) 7 | [![Coverage 8 | Status](https://coveralls.io/repos/rochacbruno/python-pagseguro/badge.png)](https://coveralls.io/r/rochacbruno/python-pagseguro) 9 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/python-pagseguro/Lobby) 10 | 11 | Donate with Paypal 12 | 13 | Integração com a API v2 de pagamentos e notificações do Pagseguro utilizando requests. 14 | 15 | Contribuidores 16 | ============== 17 | 18 | [![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/0)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/0)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/1)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/1)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/2)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/2)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/3)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/3)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/4)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/4)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/5)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/5)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/6)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/6)[![](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/images/7)](https://sourcerer.io/fame/rochacbruno/rochacbruno/python-pagseguro/links/7) 19 | 20 | Faça parte! mande um Pull Request! 21 | 22 | Instalando 23 | ========================== 24 | ```bash 25 | pip install pagseguro 26 | ``` 27 | 28 | ou 29 | 30 | 31 | ```bash 32 | pip install -e git+https://github.com/rochacbruno/python-pagseguro#egg=pagseguro 33 | ``` 34 | 35 | ou 36 | 37 | ``` 38 | git clone https://github.com/rochacbruno/python-pagseguro 39 | cd python-pagseguro 40 | pip install -r requirements.txt 41 | python setup.py install 42 | ``` 43 | 44 | Rodando os testes 45 | ================= 46 | 47 | ``` 48 | make test 49 | ``` 50 | 51 | Como usar 52 | ========= 53 | 54 | ### Carrinho de compras / ordem de venda 55 | 56 | Uma instancia de **PagSeguro** funciona como uma ordem de venda, ou um carrinho de compras. É preciso criar a instancia passando como parametro email e token. 57 | 58 | > Opcionalmente é possivel passar o parametro **data** contendo valores a serem passados diretamente para a API. 59 | 60 | ```python 61 | from pagseguro import PagSeguro 62 | 63 | pg = PagSeguro(email="seuemail@dominio.com", token="ABCDEFGHIJKLMNO") 64 | ``` 65 | 66 | ### Sandbox e Config Customizadas 67 | 68 | Ao instanciar um objecto `PagSeguro`, você poderá passar um parâmetro `config` contendo a class de configuração a ser usada pela classe. A variável `config` somente irá aceitar o tipo `dict`. 69 | 70 | ```python 71 | from pagseguro import PagSeguro 72 | 73 | config = {'sandbox': True} 74 | pg = PagSeguro(email="seuemail@dominio.com", token="ABCDEFGHIJKLMNO", config=config) 75 | ``` 76 | 77 | O seu config também pode fazer override de algumas váriaveis pré-definidas na classe de Config padrão. São elas: 78 | 79 | - CURRENCY - Moeda utilizada. Valor padrão: `'BRL'` 80 | - DATETIME_FORMAT - Formato de Data/Hora. Valor Padrão: `'%Y-%m-%dT%H:%M:%S'` 81 | - REFERENCE_PREFIX - Formato do valor de referência do produto. Valor Padrão: `'REF%s'` Obs: Nesse caso, sempre é necessário deixar o `%s` ao final do prefixo para que o mesmo seja preenchido automaticamente 82 | - USE_SHIPPING - User endereço de entrega. Valor padrão: `True` 83 | 84 | 85 | ### Configurando os dados do comprador 86 | 87 | ```python 88 | pg.sender = { 89 | "name": "Bruno Rocha", 90 | "area_code": 11, 91 | "phone": 981001213, 92 | "email": "rochacbruno@gmail.com", 93 | } 94 | ``` 95 | 96 | ### Configurando endereço de entrega 97 | ```python 98 | pg.shipping = { 99 | "type": pg.SEDEX, 100 | "street": "Av Brig Faria Lima", 101 | "number": 1234, 102 | "complement": "5 andar", 103 | "district": "Jardim Paulistano", 104 | "postal_code": "06650030", 105 | "city": "Sao Paulo", 106 | "state": "SP", 107 | "country": "BRA" 108 | } 109 | ``` 110 | 111 | Caso o **country** não seja informado o valor default será "BRA" 112 | 113 | O **type** pode ser pg.SEDEX, pg.PAC, pg.NONE 114 | > Opcionalmente pode ser numerico seguindo a tabela pagseguro: 115 | 116 | | Número | Descrição | Type | 117 | | ------ | --------- | ---- | 118 | | 1 | PAC | pg.PAC | 119 | | 2 | SEDEX | pg.SEDEX | 120 | | 3 | Nao especificado | pg.NONE | 121 | 122 | Valores opcionais para **shipping** 123 | - "cost": "123456.26" 124 | Decimal, com duas casas decimais separadas por ponto (p.e, 1234.56), maior que 0.00 e menor ou igual a 9999999.00. 125 | 126 | 127 | ### Configurando a referencia 128 | 129 | Referencia é geralmente o código que identifica a compra em seu sistema 130 | 131 | Por padrão a referencia será prefizada com "REF", mas isso pode ser alterado setando um prefixo diferente 132 | 133 | ```python 134 | pg.reference_prefix = "CODE" 135 | pg.reference_prefix = None # para desabilitar o prefixo 136 | ``` 137 | 138 | ```python 139 | pg.reference = "00123456789" 140 | print pg.reference 141 | "REF00123456789" 142 | ``` 143 | 144 | ### Configurando valor extra 145 | 146 | Especifica um valor extra que deve ser adicionado ou subtraído ao valor total do pagamento. Esse valor pode representar uma taxa extra a ser cobrada no pagamento ou um desconto a ser concedido, caso o valor seja negativo. 147 | 148 | Formato: Float (positivo ou negativo). 149 | 150 | ```python 151 | pg.extra_amount = 12.70 152 | ``` 153 | 154 | ### Inserindo produtos no carrinho 155 | 156 | O carrinho de compras é uma lista contendo dicionários que representam cada produto nos seguinte formato. 157 | 158 | #### adicionando vários produtos 159 | 160 | ```python 161 | pg.items = [ 162 | {"id": "0001", "description": "Produto 1", "amount": 354.20, "quantity": 2, "weight": 200}, 163 | {"id": "0002", "description": "Produto 2", "amount": 50, "quantity": 1, "weight": 1000} 164 | ] 165 | ``` 166 | 167 | O **weight** do produto é representado em gramas 168 | 169 | #### Adicionando um produto por vez 170 | 171 | Da forma tradicional 172 | 173 | ```python 174 | pg.items.append( 175 | {"id": "0003", "description": "Produto 3", "amount": 354.20, "quantity": 2, "weight": 200}, 176 | ) 177 | ``` 178 | 179 | Ou atraves do helper 180 | 181 | ```python 182 | pg.add_item(id="0003", description="produto 4", amount=320, quantity=1, weight=2500) 183 | ``` 184 | 185 | ### Configurando a URL de redirect 186 | 187 | Para onde o comprador será redirecionado após completar o pagamento 188 | 189 | ```python 190 | pg.redirect_url = "http://meusite.com/obrigado" 191 | ``` 192 | 193 | ### Configurando a URL de notificaçao (opcional) 194 | 195 | ```python 196 | pg.notification_url = "http://meusite.com/notification" 197 | ``` 198 | 199 | ### Efetuando o processo de checkout 200 | 201 | Depois que o carrinho esta todo configurado e com seus itens inseridos, ex: quando o seu cliente clicar no botão "efetuar pagamento", o seguinte método deverá ser executado. 202 | 203 | ```python 204 | response = pg.checkout() 205 | ``` 206 | 207 | O método checkout faz a requisição ao pagseguro e retorna um objeto PagSeguroResponse com os atributos code, date, payment_url, errors. 208 | 209 | É aconselhavel armazenar o código da transação em seu banco de dados juntamente com as informações do carrinho para seu controle interno. 210 | 211 | Utilize a **payment_url** para enviar o comprador para a página de pagamento do pagseguro. 212 | 213 | ```python 214 | return redirect(response.payment_url) 215 | ``` 216 | 217 | Após o pagamento o comprador será redirecionado de volta para os eu site através da configuração de url de retorno global ou utilizará a url especificada no parametro **redirect_url** 218 | 219 | # Notificações 220 | 221 | O PagSeguro envia as notificações para a URL que você configurou usando o protocolo HTTP, pelo método POST. 222 | 223 | Suponde que você receberá a notificação em: http://seusite.com/notification 224 | 225 | > Pseudo codigo 226 | 227 | ```python 228 | from pagseguro import PagSeguro 229 | 230 | def notification_view(request): 231 | notification_code = request.POST['notificationCode'] 232 | pg = PagSeguro(email="seuemail@dominio.com", token="ABCDEFGHIJKLMNO") 233 | notification_data = pg.check_notification(notification_code) 234 | ... 235 | ``` 236 | 237 | No exemplo acima pegamos o **notificationCode** que foi enviado através do pagseguro e fizemos uma consulta para pegar os dados da notificação, o retorno será em um dicionário Python com o seguinte formato: 238 | 239 | ```python 240 | { 241 | "date": datetime(2013, 01, 01, 18, 23, 0000), 242 | "code": "XDFD454545", 243 | "reference": "REF00123456789", 244 | "type": 1, 245 | "status": 3, 246 | "cancellationSource": "INTERNAL", 247 | ... 248 | } 249 | ``` 250 | 251 | A lista completa de valores pode ser conferida em https://pagseguro.uol.com.br/v2/guia-de-integracao/api-de-notificacoes.html 252 | 253 | 254 | # Implementações 255 | 256 | > Implementações a serem feitas, esperando o seu Pull Request!!! 257 | 258 | ## Quokka CMS 259 | [Quokka Cart PagSeguro Processor](https://github.com/pythonhub/quokka-cart/blob/master/processors/pagseguro_processor.py) 260 | 261 | ## Exemplo em Flask 262 | 263 | [FlaskSeguro](https://github.com/rochacbruno/python-pagseguro/tree/master/examples/flask) 264 | by @shyba 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Exemplos 2 | =========== 3 | Exemplos de uso do python-pagseguro. 4 | -------------------------------------------------------------------------------- /examples/flask/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/Dockerfile -------------------------------------------------------------------------------- /examples/flask/README.md: -------------------------------------------------------------------------------- 1 | FlaskSeguro 2 | ========== 3 | Exemplo do python-pagseguro utilizando Flask. 4 | 5 | Instalação 6 | ========== 7 | 8 | ```bash 9 | virtualenv env 10 | source env/bin/activate 11 | git clone https://github.com/rochacbruno/python-pagseguro.git 12 | cd python-pagseguro/examples/flask/ 13 | pip install -r requirements.txt 14 | ./run.py 15 | ``` 16 | 17 | Configuração 18 | ========== 19 | Altere o settings.cfg, ele possui as seguintes configurações por padrão: 20 | ``` 21 | EXTRA_AMOUNT = 12.12 22 | REDIRECT_URL = "http://meusite.com/obrigado" 23 | NOTIFICATION_URL = "http://meusite.com/notification" 24 | EMAIL = "seuemail@dominio.com" 25 | TOKEN = "ABCDEFGHIJKLMNO" 26 | SECRET_KEY = "s3cr3t" 27 | ``` 28 | 29 | 30 | Testes 31 | ========== 32 | 33 | ```bash 34 | ./tests.py 35 | ``` 36 | 37 | Telas 38 | ========== 39 | ![](https://raw.githubusercontent.com/shyba/python-pagseguro/master/examples/flask/screenshots/screen1.png) 40 | ![](https://raw.githubusercontent.com/shyba/python-pagseguro/master/examples/flask/screenshots/screen2.png) 41 | ![](https://raw.githubusercontent.com/shyba/python-pagseguro/master/examples/flask/screenshots/screen3.png) 42 | -------------------------------------------------------------------------------- /examples/flask/config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Config: 4 | EXTRA_AMOUNT = 12.12 5 | REDIRECT_URL = "http://meusite.com/obrigado" 6 | NOTIFICATION_URL = "http://meusite.com/notification" 7 | EMAIL = "seuemail@dominio.com" 8 | TOKEN = "ABCDEFGHIJKLMNO" 9 | SECRET_KEY = "s3cr3t" 10 | 11 | 12 | class DevelopmentConfig(Config): 13 | FLASK_ENV = 'development' 14 | 15 | 16 | CONFIG = { 17 | 'development': DevelopmentConfig 18 | } 19 | -------------------------------------------------------------------------------- /examples/flask/docker-compose.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/docker-compose.yml -------------------------------------------------------------------------------- /examples/flask/flask_seguro/__init__.py: -------------------------------------------------------------------------------- 1 | """ Application Skeleton """ 2 | from flask import Flask 3 | from flask_bootstrap import Bootstrap 4 | from config import CONFIG 5 | 6 | BOOTSTRAP = Bootstrap() 7 | 8 | 9 | def create_app(config_name): 10 | """ Factory Function """ 11 | app = Flask(__name__) 12 | app.config.from_object(CONFIG[config_name]) 13 | 14 | BOOTSTRAP.init_app(app) 15 | 16 | # call controllers 17 | from flask_seguro.controllers.main import main as main_blueprint 18 | 19 | app.register_blueprint(main_blueprint) 20 | return app 21 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/cart.py: -------------------------------------------------------------------------------- 1 | """ The file is responsable for cart in flask-webpage """ 2 | 3 | from flask import current_app as app 4 | from flask_seguro.products import Products 5 | 6 | 7 | class Cart(object): 8 | """ The classe is responsable for cart in webpage """ 9 | 10 | def __init__(self, cart_dict=None): 11 | """ Initializing class """ 12 | 13 | cart_dict = cart_dict or {} 14 | if cart_dict == {}: 15 | self.total = 0 16 | self.subtotal = 0 17 | self.items = [] 18 | else: 19 | self.total = cart_dict["total"] 20 | self.subtotal = cart_dict["subtotal"] 21 | self.items = cart_dict["items"] 22 | self.extra_amount = float(app.config['EXTRA_AMOUNT']) 23 | 24 | def to_dict(self): 25 | """ Attribute values to dict """ 26 | 27 | return { 28 | "total": self.total, 29 | "subtotal": self.subtotal, 30 | "items": self.items, 31 | "extra_amount": self.extra_amount 32 | } 33 | 34 | def change_item(self, item_id, operation): 35 | """ Remove items in cart """ 36 | 37 | product = Products().get_one(item_id) 38 | if product: 39 | if operation == 'add': 40 | self.items.append(product) 41 | elif operation == 'remove': 42 | cart_p = [x for x in self.items if x['id'] == product['id']] 43 | self.items.remove(cart_p[0]) 44 | self.update() 45 | return True 46 | else: 47 | return False 48 | 49 | def update(self): 50 | """ Remove items in cart """ 51 | 52 | subtotal = float(0) 53 | total = float(0) 54 | for product in self.items: 55 | subtotal += float(product["price"]) 56 | if subtotal > 0: 57 | total = subtotal + self.extra_amount 58 | self.subtotal = subtotal 59 | self.total = total 60 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/flask_seguro/controllers/__init__.py -------------------------------------------------------------------------------- /examples/flask/flask_seguro/controllers/main/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Main Controllers """ 3 | from flask import session 4 | from flask import jsonify 5 | from flask import request 6 | from flask import redirect 7 | from flask import render_template 8 | from flask import current_app as app 9 | 10 | from pagseguro import PagSeguro 11 | from flask_seguro.products import Products 12 | from flask_seguro.cart import Cart 13 | from .views import main 14 | 15 | 16 | @main.before_request 17 | def before_request(): 18 | if 'cart' not in session: 19 | session['cart'] = Cart().to_dict() 20 | 21 | 22 | @main.route('/') 23 | def index(): 24 | """ Index Route """ 25 | return list_products() 26 | 27 | 28 | @main.route('/cart') 29 | def cart(): 30 | """ Cart Route """ 31 | return render_template('cart.jinja2', cart=session['cart']) 32 | 33 | 34 | @main.route('/products/list') 35 | def list_products(): 36 | """ Product list """ 37 | products = Products().get_all() 38 | return render_template('products.jinja2', 39 | products=products, 40 | cart=session['cart']) 41 | 42 | 43 | @main.route('/cart/add/') 44 | def add_to_cart(item_id): 45 | """ Cart with Product """ 46 | cart = Cart(session['cart']) 47 | if cart.change_item(item_id, 'add'): 48 | session['cart'] = cart.to_dict() 49 | return list_products() 50 | 51 | 52 | @main.route('/cart/remove/') 53 | def remove_from_cart(item_id): 54 | cart = Cart(session['cart']) 55 | if cart.change_item(item_id, 'remove'): 56 | session['cart'] = cart.to_dict() 57 | return list_products() 58 | 59 | 60 | @main.route('/notification') 61 | def notification_view(request): 62 | notification_code = request.POST['notificationCode'] 63 | pg = PagSeguro(email=app.config['EMAIL'], token=app.config['TOKEN']) 64 | pg.check_notification(notification_code) 65 | # use the return of the function above to update the order 66 | 67 | 68 | @main.route('/checkout', methods=['GET']) 69 | def checkout_get(): 70 | return render_template('checkout.jinja2') 71 | 72 | 73 | @main.route('/checkout', methods=['POST']) 74 | def checkout_post(): 75 | for field in ['name', 'email', 'street', 'number', 'complement', 76 | 'district', 'postal_code', 'city', 'state']: 77 | if not request.form.get(field, False): 78 | return jsonify({'error_msg': 'Todos os campos são obrigatórios.'}) 79 | cart = Cart(session['cart']) 80 | if len(cart.items) == 0: 81 | return jsonify({'error_msg': 'Seu carrinho está vazio.'}) 82 | sender = { 83 | "name": request.form.get("name"), 84 | "email": request.form.get("email"), 85 | } 86 | shipping = { 87 | "street": request.form.get("street"), 88 | "number": request.form.get("number"), 89 | "complement": request.form.get("complement"), 90 | "district": request.form.get("district"), 91 | "postal_code": request.form.get("postal_code"), 92 | "city": request.form.get("city"), 93 | "state": request.form.get("state"), 94 | "country": 'BRA' 95 | } 96 | pagseguro = checkout_pg(sender, shipping, cart) 97 | response = pagseguro.checkout() 98 | return redirect(response.payment_url) 99 | 100 | 101 | def checkout_pg(sender, shipping, cart): 102 | pagseguro = PagSeguro(email=app.config['EMAIL'], token=app.config['TOKEN']) 103 | pagseguro.sender = sender 104 | shipping['type'] = pagseguro.SEDEX 105 | pagseguro.shipping = shipping 106 | pagseguro.extra_amount = "%.2f" % float(app.config['EXTRA_AMOUNT']) 107 | pagseguro.redirect_url = app.config['REDIRECT_URL'] 108 | pagseguro.notification_url = app.config['NOTIFICATION_URL'] 109 | pagseguro.items = cart.items 110 | for item in cart.items: 111 | item['amount'] = "%.2f" % float(app.config['EXTRA_AMOUNT']) 112 | return pagseguro 113 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/controllers/main/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | 6 | from . import views # noqa 7 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/products.py: -------------------------------------------------------------------------------- 1 | class Products: 2 | def __init__(self): 3 | self.products = [ 4 | { 5 | "id": "0001", 6 | "description": "Produto 1", 7 | "amount": 1.00, 8 | "quantity": 1, 9 | "weight": 200, 10 | "price": 10.10 11 | }, 12 | { 13 | "id": "0002", 14 | "description": "Produto 2", 15 | "amount": 50, 16 | "quantity": 1, 17 | "weight": 1000, 18 | "price": 10.50 19 | }, 20 | ] 21 | 22 | def get_all(self): 23 | return self.products 24 | 25 | def get_one(self, item_id): 26 | p = [p for p in self.products if p['id'] == item_id] 27 | if len(p) > 0: 28 | return p[0] 29 | else: 30 | return False 31 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/static/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | .products{ 5 | margin-top: 100px !important; 6 | } -------------------------------------------------------------------------------- /examples/flask/flask_seguro/templates/base.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | 3 | {% block styles %} 4 | {{super()}} 5 | 6 | {% endblock %} 7 | 8 | {% block title %}{% endblock %} 9 | 10 | {% block navbar %} 11 | {% include 'menu.jinja2' %} 12 | {% endblock %} 13 | 14 | {% block content %} 15 |
16 |
17 |

{{ self.title() }}

18 | {% block subcontent %}{% endblock %} 19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /examples/flask/flask_seguro/templates/cart.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'base.jinja2' %} 2 | 3 | {% block title %}Carrinho{% endblock %} 4 | 5 | {% block subcontent %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for item in cart['items'] %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 | 21 |
DescriçãoValor
{{item['description']}}R${{'%0.2f'| format(item['price']|float)}}
22 |

Subtotal: R${{'%0.2f'| format(cart.subtotal|float)}}

23 |

Taxa Extra: R${{'%0.2f'| format(cart.extra_amount|float)}}

24 |

Total: R${{'%0.2f'| format(cart.total|float)}}

25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/templates/checkout.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'base.jinja2' %} 2 | 3 | {% block title %}Pagamento{% endblock %} 4 | 5 | {% block subcontent %} 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 | 44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /examples/flask/flask_seguro/templates/menu.jinja2: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /examples/flask/flask_seguro/templates/products.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'base.jinja2' %} 2 | {% block title %}Produtos{% endblock %} 3 | 4 | {% block subcontent %} 5 | {% for product in products %} 6 |
7 |
8 | {{product.description}}
9 | R${{product.price}}
10 | {% if product not in cart['items'] %} 11 | 12 | 13 | {% else %} 14 | 15 | 16 | {% endif %} 17 | 18 | 19 |
20 |
21 | {% endfor %} 22 | {% endblock%} 23 | -------------------------------------------------------------------------------- /examples/flask/requirements.txt: -------------------------------------------------------------------------------- 1 | arrow==0.12.1 2 | astroid==2.0.1 3 | atomicwrites==1.1.5 4 | attrs==18.1.0 5 | certifi==2018.4.16 6 | chardet==3.0.4 7 | click==6.7 8 | coverage==4.5.1 9 | dominate==2.3.1 10 | Flask==1.0.2 11 | Flask-Bootstrap==3.3.7.1 12 | idna==2.7 13 | isort==4.3.4 14 | itsdangerous==0.24 15 | Jinja2==2.10 16 | lazy-object-proxy==1.3.1 17 | MarkupSafe==1.0 18 | mccabe==0.6.1 19 | more-itertools==4.2.0 20 | pagseguro==0.3.2 21 | pluggy==0.6.0 22 | py==1.5.4 23 | pylint==2.0.0 24 | pytest==3.6.3 25 | pytest-cov==2.5.1 26 | pytest-sugar==0.9.1 27 | python-dateutil==2.7.3 28 | requests==2.19.1 29 | six==1.11.0 30 | termcolor==1.1.0 31 | urllib3==1.23 32 | visitor==0.1.3 33 | Werkzeug==0.14.1 34 | wrapt==1.10.11 35 | xmltodict==0.11.0 36 | -------------------------------------------------------------------------------- /examples/flask/run.py: -------------------------------------------------------------------------------- 1 | from flask_seguro import create_app 2 | 3 | app = create_app('development') 4 | 5 | 6 | @app.shell_context_processor 7 | def make_shell_context(): 8 | return dict(app=app) 9 | -------------------------------------------------------------------------------- /examples/flask/screenshots/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/screenshots/screen1.png -------------------------------------------------------------------------------- /examples/flask/screenshots/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/screenshots/screen2.png -------------------------------------------------------------------------------- /examples/flask/screenshots/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/examples/flask/screenshots/screen3.png -------------------------------------------------------------------------------- /examples/flask/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import unittest 5 | import tempfile 6 | import flask 7 | from flask import json 8 | 9 | from flask_seguro.products import Products 10 | from flask_seguro import create_app 11 | 12 | 13 | class FlasKSeguroTestCase(unittest.TestCase): 14 | def setUp(self): 15 | self._current_app = create_app('development') 16 | 17 | self.db_fd, self._current_app.config['DATABASE'] = tempfile.mkstemp() 18 | self._current_app.config['TESTING'] = True 19 | self.app = self._current_app.test_client() 20 | # flaskr.init_db() 21 | 22 | def tearDown(self): 23 | os.close(self.db_fd) 24 | os.unlink(self._current_app.config['DATABASE']) 25 | 26 | def list_products(self): 27 | return Products().get_all() 28 | 29 | def check_cart_fields(self, response): 30 | cart = flask.session['cart'] 31 | self.assertNotIn('error_msg', cart) 32 | self.assertIn('total', cart) 33 | self.assertIn('subtotal', cart) 34 | return cart 35 | 36 | def test_retrieve_cart_and_add_remove_item(self): 37 | with self._current_app.test_client() as c: 38 | response = c.get('/') 39 | session = flask.session 40 | self.assertIn('cart', session) 41 | self.assertEquals(0, len(session['cart']['items'])) 42 | 43 | products = self.list_products() 44 | 45 | response = c.get('/cart/add/%s' % (products[0]['id'])) 46 | self.assertEquals(1, len(session['cart']['items'])) 47 | cart = self.check_cart_fields(response) 48 | self.assertEquals( 49 | float(cart['subtotal']), float(products[0]['price'])) 50 | 51 | response = c.get('/cart/remove/%s' % (products[0]['id'])) 52 | self.assertEquals(0, len(session['cart']['items'])) 53 | cart = self.check_cart_fields(response) 54 | self.assertEquals(0, float(cart['total'])) 55 | self.assertEquals(float(cart['total']), float(cart['subtotal'])) 56 | 57 | def checkout(self, data, c, decode_json=True): 58 | response = c.post('/checkout', data=data) 59 | if decode_json: 60 | response = json.loads(response.data) 61 | return response 62 | 63 | def test_checkout(self): 64 | with self._current_app.test_client() as c: 65 | response = c.get('/') 66 | session = flask.session 67 | 68 | self.assertEquals(0, len(session['cart']['items'])) 69 | 70 | data = { 71 | "name": "Victor Shyba", 72 | "email": "teste@example.com", 73 | "street": "Av Brig Faria Lima", 74 | "number": 1234, 75 | "complement": "5 andar", 76 | "district": "Jardim Paulistano", 77 | "postal_code": "06650030", 78 | "city": "Sao Paulo", 79 | "state": "SP" 80 | } 81 | 82 | response = self.checkout(data, c) 83 | self.assertIn('error_msg', response) 84 | self.assertEquals( 85 | u'Seu carrinho está vazio.', response['error_msg']) 86 | 87 | response = self.checkout({}, c) 88 | self.assertIn('error_msg', response) 89 | self.assertEquals( 90 | u'Todos os campos são obrigatórios.', response['error_msg']) 91 | 92 | products = self.list_products() 93 | response = c.get('/cart/add/%s' % (products[0]['id'])) 94 | self.assertEquals(1, len(session['cart']['items'])) 95 | 96 | response = self.checkout(data, c, decode_json=False) 97 | self.assertEquals(302, response.status_code) 98 | self.assertIn('pagseguro', response.location) 99 | 100 | def test_cart_view(self): 101 | response = self.app.get('/cart') 102 | self.assertEquals(200, response.status_code) 103 | 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /pagseguro/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | import requests 4 | 5 | from .config import Config 6 | from .utils import is_valid_email, is_valid_cpf, is_valid_cnpj 7 | from .parsers import (PagSeguroNotificationResponse, 8 | PagSeguroPreApprovalNotificationResponse, 9 | PagSeguroPreApprovalCancel, 10 | PagSeguroCheckoutSession, 11 | PagSeguroPreApprovalPayment, 12 | PagSeguroCheckoutResponse, 13 | PagSeguroTransactionSearchResult, 14 | PagSeguroPreApproval, 15 | PagSeguroPreApprovalSearch) 16 | 17 | logger = logging.getLogger() 18 | 19 | 20 | class PagSeguro(object): 21 | """ Pag Seguro V2 wrapper """ 22 | 23 | PAC = 1 24 | SEDEX = 2 25 | NONE = 3 26 | 27 | def __init__(self, email, token, data=None, config=None): 28 | 29 | config = config or {} 30 | if not type(config) == dict: 31 | raise Exception('Malformed config dict param') 32 | 33 | self.config = Config(**config) 34 | 35 | self.data = {} 36 | self.data['email'] = email 37 | self.data['token'] = token 38 | 39 | if data and isinstance(data, dict): 40 | self.data.update(data) 41 | 42 | self.items = [] 43 | self.sender = {} 44 | self.shipping = {} 45 | self._reference = "" 46 | self.extra_amount = None 47 | self.redirect_url = None 48 | self.notification_url = None 49 | self.abandon_url = None 50 | self.credit_card = {} 51 | self.pre_approval = {} 52 | self.checkout_session = None 53 | self.payment = {} 54 | 55 | def build_checkout_params(self, **kwargs): 56 | """ build a dict with params """ 57 | params = kwargs or {} 58 | if self.sender: 59 | params['senderName'] = self.sender.get('name') 60 | params['senderAreaCode'] = self.sender.get('area_code') 61 | params['senderPhone'] = self.sender.get('phone') 62 | params['senderEmail'] = is_valid_email(self.sender.get('email')) 63 | params['senderCPF'] = is_valid_cpf(self.sender.get('cpf')) 64 | params['senderCNPJ'] = is_valid_cnpj(self.sender.get('cnpj')) 65 | params['senderBornDate'] = self.sender.get('born_date') 66 | params['senderHash'] = self.sender.get('hash') 67 | 68 | if self.config.USE_SHIPPING: 69 | if self.shipping: 70 | params['shippingType'] = self.shipping.get('type') 71 | params['shippingAddressStreet'] = self.shipping.get('street') 72 | params['shippingAddressNumber'] = self.shipping.get('number') 73 | params['shippingAddressComplement'] = self.shipping.get( 74 | 'complement') 75 | params['shippingAddressDistrict'] = self.shipping.get( 76 | 'district') 77 | params['shippingAddressPostalCode'] = self.shipping.get( 78 | 'postal_code') 79 | params['shippingAddressCity'] = self.shipping.get('city') 80 | params['shippingAddressState'] = self.shipping.get('state') 81 | params['shippingAddressCountry'] = self.shipping.get('country', 82 | 'BRA') 83 | if self.shipping.get('cost'): 84 | params['shippingCost'] = self.shipping.get('cost') 85 | else: 86 | params['shippingAddressRequired'] = 'false' 87 | 88 | if self.extra_amount: 89 | params['extraAmount'] = self.extra_amount 90 | 91 | params['reference'] = self.reference 92 | params['receiverEmail'] = self.data['email'] 93 | 94 | if self.redirect_url: 95 | params['redirectURL'] = self.redirect_url 96 | 97 | if self.notification_url: 98 | params['notificationURL'] = self.notification_url 99 | 100 | if self.abandon_url: 101 | params['abandonURL'] = self.abandon_url 102 | 103 | for i, item in enumerate(self.items, 1): 104 | params['itemId%s' % i] = item.get('id') 105 | params['itemDescription%s' % i] = item.get('description') 106 | params['itemAmount%s' % i] = item.get('amount') 107 | params['itemQuantity%s' % i] = item.get('quantity') 108 | params['itemWeight%s' % i] = item.get('weight') 109 | params['itemShippingCost%s' % i] = item.get('shipping_cost') 110 | 111 | if self.payment: 112 | 113 | params['paymentMethod'] = self.payment.get('method') 114 | params['paymentMode'] = self.payment.get('mode') 115 | 116 | if self.credit_card: 117 | params['billingAddressCountry'] = 'BRA' 118 | 119 | credit_card_keys_map = [ 120 | ('creditCardToken', 'credit_card_token'), 121 | ('installmentQuantity', 'installment_quantity'), 122 | ('installmentValue', 'installment_value'), 123 | ('noInterestInstallmentQuantity', 124 | 'no_interest_installment_quantity'), 125 | ('creditCardHolderName', 'card_holder_name'), 126 | ('creditCardHolderCPF', 'card_holder_cpf'), 127 | ('creditCardHolderBirthDate', 'card_holder_birth_date'), 128 | ('creditCardHolderAreaCode', 'card_holder_area_code'), 129 | ('creditCardHolderPhone', 'card_holder_phone'), 130 | ('billingAddressStreet', 'billing_address_street'), 131 | ('billingAddressNumber', 'billing_address_number'), 132 | ('billingAddressComplement', 'billing_address_complement'), 133 | ('billingAddressDistrict', 'billing_address_district'), 134 | ('billingAddressPostalCode', 'billing_address_postal_code'), 135 | ('billingAddressCity', 'billing_address_city'), 136 | ('billingAddressState', 'billing_address_state'), 137 | ] 138 | 139 | for key_to_set, key_to_get in credit_card_keys_map: 140 | params[key_to_set] = self.credit_card.get(key_to_get) 141 | 142 | if self.pre_approval: 143 | 144 | params['preApprovalCharge'] = self.pre_approval.get('charge') 145 | params['preApprovalName'] = self.pre_approval.get('name') 146 | params['preApprovalDetails'] = self.pre_approval.get('details') 147 | params['preApprovalAmountPerPayment'] = self.pre_approval.get( 148 | 'amount_per_payment') 149 | params['preApprovalMaxAmountPerPayment'] = self.pre_approval.get( 150 | 'max_amount_per_payment') 151 | params['preApprovalPeriod'] = self.pre_approval.get('period') 152 | params['preApprovalMaxPaymentsPerPeriod'] = self.pre_approval.get( 153 | 'max_payments_per_period') 154 | params['preApprovalMaxAmountPerPeriod'] = self.pre_approval.get( 155 | 'max_amount_per_period') 156 | params['preApprovalInitialDate'] = self.pre_approval.get( 157 | 'initial_date') 158 | params['preApprovalFinalDate'] = self.pre_approval.get( 159 | 'final_date') 160 | params['preApprovalMaxTotalAmount'] = self.pre_approval.get( 161 | 'max_total_amount') 162 | 163 | self.data.update(params) 164 | self.clean_none_params() 165 | 166 | def build_pre_approval_payment_params(self, **kwargs): 167 | """ build a dict with params """ 168 | 169 | params = kwargs or {} 170 | 171 | params['reference'] = self.reference 172 | params['preApprovalCode'] = self.code 173 | 174 | for i, item in enumerate(self.items, 1): 175 | params['itemId%s' % i] = item.get('id') 176 | params['itemDescription%s' % i] = item.get('description') 177 | params['itemAmount%s' % i] = item.get('amount') 178 | params['itemQuantity%s' % i] = item.get('quantity') 179 | params['itemWeight%s' % i] = item.get('weight') 180 | params['itemShippingCost%s' % i] = item.get('shipping_cost') 181 | 182 | self.data.update(params) 183 | self.clean_none_params() 184 | 185 | def clean_none_params(self): 186 | self.data = \ 187 | {k: v for k, v in self.data.items() if v or isinstance(v, bool)} 188 | 189 | @property 190 | def reference_prefix(self): 191 | return self.config.REFERENCE_PREFIX or "%s" 192 | 193 | @reference_prefix.setter 194 | def reference_prefix(self, value): 195 | self.config.REFERENCE_PREFIX = (value or "") + "%s" 196 | 197 | @property 198 | def reference(self): 199 | return self.reference_prefix % self._reference 200 | 201 | @reference.setter 202 | def reference(self, value): 203 | if not isinstance(value, str): 204 | value = str(value) 205 | if value.startswith(self.reference_prefix): 206 | value = value[len(self.reference_prefix):] 207 | self._reference = value 208 | 209 | def get(self, url): 210 | """ do a get transaction """ 211 | return requests.get(url, params=self.data, headers=self.config.HEADERS) 212 | 213 | def post(self, url): 214 | """ do a post request """ 215 | return requests.post(url, data=self.data, headers=self.config.HEADERS) 216 | 217 | def checkout(self, transparent=False, **kwargs): 218 | """ create a pagseguro checkout """ 219 | self.data['currency'] = self.config.CURRENCY 220 | self.build_checkout_params(**kwargs) 221 | if transparent: 222 | response = self.post(url=self.config.TRANSPARENT_CHECKOUT_URL) 223 | else: 224 | response = self.post(url=self.config.CHECKOUT_URL) 225 | return PagSeguroCheckoutResponse(response.content, config=self.config) 226 | 227 | def transparent_checkout_session(self): 228 | response = self.post(url=self.config.SESSION_CHECKOUT_URL) 229 | return PagSeguroCheckoutSession(response.content, 230 | config=self.config).session_id 231 | 232 | def check_notification(self, code): 233 | """ check a notification by its code """ 234 | response = self.get(url=self.config.NOTIFICATION_URL % code) 235 | return PagSeguroNotificationResponse(response.content, self.config) 236 | 237 | def check_pre_approval_notification(self, code): 238 | """ check a notification by its code """ 239 | response = self.get( 240 | url=self.config.PRE_APPROVAL_NOTIFICATION_URL % code) 241 | return PagSeguroPreApprovalNotificationResponse( 242 | response.content, self.config) 243 | 244 | def pre_approval_ask_payment(self, **kwargs): 245 | """ ask form a subscribe payment """ 246 | self.build_pre_approval_payment_params(**kwargs) 247 | response = self.post(url=self.config.PRE_APPROVAL_PAYMENT_URL) 248 | return PagSeguroPreApprovalPayment(response.content, self.config) 249 | 250 | def pre_approval_cancel(self, code): 251 | """ cancel a subscribe """ 252 | response = self.get(url=self.config.PRE_APPROVAL_CANCEL_URL % code) 253 | return PagSeguroPreApprovalCancel(response.content, self.config) 254 | 255 | def check_transaction(self, code): 256 | """ check a transaction by its code """ 257 | response = self.get(url=self.config.TRANSACTION_URL % code) 258 | return PagSeguroNotificationResponse(response.content, self.config) 259 | 260 | def query_transactions(self, initial_date, final_date, 261 | page=None, 262 | max_results=None): 263 | """ query transaction by date range """ 264 | last_page = False 265 | results = [] 266 | while last_page is False: 267 | search_result = self._consume_query_transactions( 268 | initial_date, final_date, page, max_results) 269 | results.extend(search_result.transactions) 270 | if search_result.current_page is None or \ 271 | search_result.total_pages is None or \ 272 | search_result.current_page == search_result.total_pages: 273 | last_page = True 274 | else: 275 | page = search_result.current_page + 1 276 | 277 | return results 278 | 279 | def _consume_query_transactions(self, initial_date, final_date, 280 | page=None, 281 | max_results=None): 282 | querystring = { 283 | 'initialDate': initial_date.strftime('%Y-%m-%dT%H:%M'), 284 | 'finalDate': final_date.strftime('%Y-%m-%dT%H:%M'), 285 | 'page': page, 286 | 'maxPageResults': max_results, 287 | } 288 | self.data.update(querystring) 289 | self.clean_none_params() 290 | response = self.get(url=self.config.QUERY_TRANSACTION_URL) 291 | return PagSeguroTransactionSearchResult(response.content, self.config) 292 | 293 | def query_pre_approvals(self, initial_date, final_date, page=None, 294 | max_results=None): 295 | """ query pre-approvals by date range """ 296 | last_page = False 297 | results = [] 298 | while last_page is False: 299 | search_result = self._consume_query_pre_approvals( 300 | initial_date, final_date, page, max_results) 301 | results.extend(search_result.pre_approvals) 302 | if search_result.current_page is None or \ 303 | search_result.total_pages is None or \ 304 | search_result.current_page == search_result.total_pages: 305 | last_page = True 306 | else: 307 | page = search_result.current_page + 1 308 | 309 | return results 310 | 311 | def _consume_query_pre_approvals(self, initial_date, final_date, page=None, 312 | max_results=None): 313 | querystring = { 314 | 'initialDate': initial_date.strftime('%Y-%m-%dT%H:%M'), 315 | 'finalDate': final_date.strftime('%Y-%m-%dT%H:%M'), 316 | 'page': page, 317 | 'maxPageResults': max_results, 318 | } 319 | 320 | self.data.update(querystring) 321 | self.clean_none_params() 322 | 323 | response = self.get(url=self.config.QUERY_PRE_APPROVAL_URL) 324 | return PagSeguroPreApprovalSearch(response.content, self.config) 325 | 326 | def query_pre_approvals_by_code(self, code): 327 | """ query pre-approvals by code """ 328 | result = self._consume_query_pre_approvals_by_code(code) 329 | return result 330 | 331 | def _consume_query_pre_approvals_by_code(self, code): 332 | 333 | response = self.get( 334 | url='%s/%s' % (self.config.QUERY_PRE_APPROVAL_URL, code) 335 | ) 336 | return PagSeguroPreApproval(response.content, self.config) 337 | 338 | def add_item(self, **kwargs): 339 | self.items.append(kwargs) 340 | -------------------------------------------------------------------------------- /pagseguro/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class Config(dict): 3 | def __init__(self, **kwargs): 4 | sandbox = kwargs.pop('sandbox', False) 5 | 6 | base_url = 'https://ws.pagseguro.uol.com.br' 7 | payment_host = 'https://pagseguro.uol.com.br' 8 | if sandbox: 9 | base_url = 'https://ws.sandbox.pagseguro.uol.com.br' 10 | payment_host = 'https://sandbox.pagseguro.uol.com.br' 11 | 12 | # prefixes/suffixes 13 | version = '/v2/' 14 | checkout_suffix = '{}checkout'.format(version) 15 | session_checkout_suffix = '{}sessions/'.format(version) 16 | notification_suffix = '{}transactions/notifications/%s'.format(version) 17 | pre_approval_notification_suffix = ('{}pre-approvals/' 18 | 'notifications/%s'.format(version)) 19 | transaction_suffix = '{}transactions/%s'.format(version) 20 | query_transaction_suffix = '{}transactions'.format(version) 21 | ctype = 'application/x-www-form-urlencoded; charset=UTF-8' 22 | 23 | # default config settings 24 | defaults = dict( 25 | PRE_APPROVAL_PAYMENT_URL='{}{}pre-approvals/payment'.format( 26 | base_url, version), 27 | PRE_APPROVAL_CANCEL_URL='{}{}pre-approvals/cancel/%s'.format( 28 | base_url, version), 29 | SESSION_CHECKOUT_URL='{}{}'.format( 30 | base_url, session_checkout_suffix), 31 | TRANSPARENT_CHECKOUT_URL='{}{}'.format( 32 | base_url, query_transaction_suffix), 33 | CHECKOUT_URL='{}{}'.format(base_url, checkout_suffix), 34 | NOTIFICATION_URL='{}{}'.format(base_url, notification_suffix), 35 | PRE_APPROVAL_NOTIFICATION_URL='{}{}'.format( 36 | base_url, pre_approval_notification_suffix), 37 | TRANSACTION_URL='{}{}'.format(base_url, transaction_suffix), 38 | QUERY_TRANSACTION_URL='{}{}'.format( 39 | base_url, query_transaction_suffix), 40 | QUERY_PRE_APPROVAL_URL='{}{}pre-approvals'.format( 41 | base_url, version), 42 | CURRENCY='BRL', 43 | HEADERS={'Content-Type': ctype}, 44 | PAYMENT_URL='{}{}/payment.html?code=%s'.format( 45 | payment_host, checkout_suffix), 46 | DATETIME_FORMAT='%Y-%m-%dT%H:%M:%S', 47 | REFERENCE_PREFIX='REF%s', 48 | USE_SHIPPING=True, 49 | ) 50 | 51 | kwargs = {key.upper(): val for key, val in kwargs.items()} 52 | keys = defaults.keys() 53 | for key in keys: 54 | # only add override keys to properties 55 | value = kwargs.pop(key, defaults[key]) 56 | setattr(self, key, value) 57 | 58 | def __getitem__(self, key): 59 | return getattr(self, key) 60 | 61 | def __setitem__(self, key, value): 62 | return setattr(self, key, value) 63 | -------------------------------------------------------------------------------- /pagseguro/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | class PagSeguroValidationError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /pagseguro/parsers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from .utils import parse_date 5 | from .config import Config 6 | 7 | import xmltodict 8 | 9 | logger = logging.getLogger() 10 | 11 | 12 | class XMLParser(object): 13 | def __init__(self, xml, config=None): 14 | self.xml = xml 15 | self.errors = None 16 | if config is None: 17 | config = Config() 18 | self.config = config 19 | self.parse_xml(xml) 20 | logger.debug(self.__dict__) 21 | 22 | def parse_xml(self, xml): 23 | try: 24 | parsed = xmltodict.parse(xml, encoding="iso-8859-1") 25 | except Exception as e: 26 | logger.debug('Cannot parse the returned xml "%s" -> "%s"', xml, e) 27 | parsed = {} 28 | 29 | if 'errors' in parsed: 30 | self.errors = parsed['errors']['error'] 31 | 32 | return parsed 33 | 34 | 35 | class PagSeguroNotificationResponse(XMLParser): 36 | def __getitem__(self, key): 37 | getattr(self, key, None) 38 | 39 | def parse_xml(self, xml): 40 | parsed = super(PagSeguroNotificationResponse, self).parse_xml(xml) 41 | if self.errors: 42 | return 43 | transaction = parsed.get('transaction', {}) 44 | for k, v in transaction.items(): 45 | setattr(self, k, v) 46 | 47 | 48 | class PagSeguroPreApprovalNotificationResponse(XMLParser): 49 | def __getitem__(self, key): 50 | getattr(self, key, None) 51 | 52 | def parse_xml(self, xml): 53 | parsed = super(PagSeguroPreApprovalNotificationResponse, 54 | self).parse_xml(xml) 55 | if self.errors: 56 | return 57 | transaction = parsed.get('transaction', {}) 58 | for k, v in transaction.items(): 59 | setattr(self, k, v) 60 | 61 | 62 | class PagSeguroPreApprovalCancel(XMLParser): 63 | def __getitem__(self, key): 64 | getattr(self, key, None) 65 | 66 | def parse_xml(self, xml): 67 | parsed = super(PagSeguroPreApprovalCancel, self).parse_xml(xml) 68 | if self.errors: 69 | return 70 | transaction = parsed.get('transaction', {}) 71 | for k, v in transaction.items(): 72 | setattr(self, k, v) 73 | 74 | 75 | class PagSeguroCheckoutSession(XMLParser): 76 | def __init__(self, xml, config=None): 77 | self.session_id = None 78 | super(PagSeguroCheckoutSession, self).__init__(xml, config) 79 | 80 | def parse_xml(self, xml): 81 | parsed = super(PagSeguroCheckoutSession, self).parse_xml(xml) 82 | if self.errors: 83 | return 84 | session = parsed.get('session', {}) 85 | self.session_id = session.get('id') 86 | 87 | 88 | class PagSeguroPreApprovalPayment(XMLParser): 89 | def __init__(self, xml, config=None): 90 | self.code = None 91 | super(PagSeguroPreApprovalPayment, self).__init__(xml, config) 92 | 93 | def parse_xml(self, xml): 94 | parsed = super(PagSeguroPreApprovalPayment, self).parse_xml(xml) 95 | if self.errors: 96 | return 97 | result = parsed.get('result', {}) 98 | self.code = result.get('transactionCode') 99 | self.date = parse_date(result.get('date')) 100 | 101 | 102 | class PagSeguroCheckoutResponse(XMLParser): 103 | def __init__(self, xml, config=None): 104 | self.code = None 105 | self.date = None 106 | self.payment_url = None 107 | self.payment_link = None 108 | self.transaction = None 109 | super(PagSeguroCheckoutResponse, self).__init__(xml, config) 110 | 111 | def parse_xml(self, xml): 112 | parsed = super(PagSeguroCheckoutResponse, self).parse_xml(xml) 113 | if self.errors: 114 | return 115 | checkout = parsed.get('checkout', {}) 116 | self.code = checkout.get('code') 117 | self.date = parse_date(checkout.get('date')) 118 | 119 | self.payment_url = self.config.PAYMENT_URL % self.code 120 | 121 | # this is used only for transparent checkout process 122 | self.transaction = parsed.get('transaction', {}) 123 | self.payment_link = self.transaction.get('paymentLink') 124 | 125 | 126 | class PagSeguroTransactionSearchResult(XMLParser): 127 | current_page = None 128 | total_pages = None 129 | results_in_page = None 130 | transactions = [] 131 | 132 | def __getitem__(self, key): 133 | getattr(self, key, None) 134 | 135 | def parse_xml(self, xml): 136 | parsed = super(PagSeguroTransactionSearchResult, self).parse_xml(xml) 137 | if self.errors: 138 | return 139 | search_result = parsed.get('transactionSearchResult', {}) 140 | self.transactions = search_result.get('transactions', {}) 141 | self.transactions = self.transactions.get('transaction', []) 142 | if not isinstance(self.transactions, list): 143 | self.transactions = [self.transactions] 144 | self.current_page = search_result.get('currentPage', None) 145 | if self.current_page is not None: 146 | self.current_page = int(self.current_page) 147 | self.results_in_page = search_result.get('resultsInThisPage', None) 148 | if self.results_in_page is not None: 149 | self.results_in_page = int(self.results_in_page) 150 | self.total_pages = search_result.get('totalPages', None) 151 | if self.total_pages is not None: 152 | self.total_pages = int(self.total_pages) 153 | 154 | 155 | class PagSeguroPreApproval(XMLParser): 156 | 157 | def __getitem__(self, key): 158 | getattr(self, key, None) 159 | 160 | def parse_xml(self, xml): 161 | parsed = super(PagSeguroPreApproval, self).parse_xml(xml) 162 | if self.errors: 163 | return 164 | result = parsed.get('preApproval', {}) 165 | self.name = result.get('name', None) 166 | self.code = result.get('code', None) 167 | self.date = parse_date(result.get('date')) 168 | self.tracker = result.get('tracker', None) 169 | self.status = result.get('status', None) 170 | self.reference = result.get('reference', None) 171 | self.last_event_date = result.get('lastEventDate', None) 172 | self.charge = result.get('charge', None) 173 | self.sender = result.get('sender', {}) 174 | 175 | 176 | class PagSeguroPreApprovalSearch(XMLParser): 177 | 178 | current_page = None 179 | total_pages = None 180 | results_in_page = None 181 | pre_approvals = [] 182 | 183 | def __getitem__(self, key): 184 | getattr(self, key, None) 185 | 186 | def parse_xml(self, xml): 187 | parsed = super(PagSeguroPreApprovalSearch, self).parse_xml(xml) 188 | if self.errors: 189 | return 190 | search_result = parsed.get('preApprovalSearchResult', {}) 191 | self.pre_approvals = search_result.get('preApprovals', {}) 192 | self.pre_approvals = self.pre_approvals.get('preApproval', []) 193 | if not isinstance(self.pre_approvals, list): 194 | self.pre_approvals = [self.pre_approvals] 195 | self.current_page = search_result.get('currentPage', None) 196 | if self.current_page is not None: 197 | self.current_page = int(self.current_page) 198 | self.results_in_page = search_result.get('resultsInThisPage', None) 199 | if self.results_in_page is not None: 200 | self.results_in_page = int(self.results_in_page) 201 | self.total_pages = search_result.get('totalPages', None) 202 | if self.total_pages is not None: 203 | self.total_pages = int(self.total_pages) 204 | -------------------------------------------------------------------------------- /pagseguro/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import re 3 | 4 | import arrow 5 | 6 | from .exceptions import PagSeguroValidationError 7 | 8 | 9 | def parse_date(date_str): 10 | return arrow.get(date_str).datetime 11 | 12 | # Validators 13 | EMPTY_VALUES = (None, '', [], (), {}) 14 | 15 | 16 | def is_valid_email(value): 17 | user_regex = re.compile( 18 | r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" 19 | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013' 20 | r"""\014\016-\177])*"$)""", re.IGNORECASE) 21 | domain_regex = re.compile( 22 | r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|' 23 | r'[A-Z0-9-]{2,})$|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|' 24 | r'2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) 25 | domain_whitelist = ['localhost'] 26 | 27 | if not value or '@' not in value: 28 | raise PagSeguroValidationError(u'Email inválido') 29 | 30 | user_part, domain_part = value.rsplit('@', 1) 31 | 32 | if not user_regex.match(user_part): 33 | raise PagSeguroValidationError(u'Email inválido') 34 | 35 | if (domain_part not in domain_whitelist and 36 | not domain_regex.match(domain_part)): 37 | # Try for possible IDN domain-part 38 | try: 39 | domain_part = domain_part.encode('idna').decode('ascii') 40 | if not domain_regex.match(domain_part): 41 | raise PagSeguroValidationError(u'Email inválido') 42 | else: 43 | return value 44 | except UnicodeError: 45 | pass 46 | raise PagSeguroValidationError(u'Email inválido') 47 | return value 48 | 49 | 50 | def DV_maker(v): 51 | if v >= 2: 52 | return 11 - v 53 | return 0 54 | 55 | 56 | def is_valid_cpf(value): 57 | error_messages = { 58 | 'invalid': u"CPF Inválido", 59 | 'max_digits': (u"CPF possui 11 dígitos (somente números) ou 14" 60 | u" (com pontos e hífen)"), 61 | 'digits_only': (u"Digite um CPF com apenas números ou com ponto e " 62 | u"hífen"), 63 | } 64 | 65 | if value in EMPTY_VALUES: 66 | return u'' 67 | orig_value = value[:] 68 | if not value.isdigit(): 69 | value = re.sub("[-\.]", "", value) 70 | try: 71 | int(value) 72 | except ValueError: 73 | raise PagSeguroValidationError(error_messages['digits_only']) 74 | if len(value) != 11: 75 | raise PagSeguroValidationError(error_messages['max_digits']) 76 | orig_dv = value[-2:] 77 | 78 | new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(range(10, 1, - 79 | 1))]) 80 | new_1dv = DV_maker(new_1dv % 11) 81 | value = value[:-2] + str(new_1dv) + value[-1] 82 | new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(range(11, 1, - 83 | 1))]) 84 | new_2dv = DV_maker(new_2dv % 11) 85 | value = value[:-1] + str(new_2dv) 86 | if value[-2:] != orig_dv: 87 | raise PagSeguroValidationError(error_messages['invalid']) 88 | 89 | return orig_value 90 | 91 | 92 | def is_valid_cnpj(value): 93 | 94 | error_messages = { 95 | 'invalid': u"CNPJ Inválido", 96 | 'max_digits': (u"CNPJ possui 14 dígitos (somente números) ou 14" 97 | u" (com pontos e hífen)"), 98 | 'digits_only': ( 99 | u"Digite um CNPJ com apenas números ou com ponto, barra " 100 | u"hífen"), 101 | } 102 | 103 | if value in EMPTY_VALUES: 104 | return u'' 105 | if not value.isdigit(): 106 | value = re.sub("[-/\.]", "", value) 107 | orig_value = value[:] 108 | try: 109 | int(value) 110 | except ValueError: 111 | raise PagSeguroValidationError(error_messages['digits_only']) 112 | if len(value) != 14: 113 | raise PagSeguroValidationError(error_messages['max_digits']) 114 | 115 | orig_dv = value[-2:] 116 | 117 | new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range( 118 | 5, 1, -1)) + list(range(9, 1, -1)))]) 119 | new_1dv = DV_maker(new_1dv % 11) 120 | value = value[:-2] + str(new_1dv) + value[-1] 121 | new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range( 122 | 6, 1, -1)) + list(range(9, 1, -1)))]) 123 | new_2dv = DV_maker(new_2dv % 11) 124 | value = value[:-1] + str(new_2dv) 125 | if value[-2:] != orig_dv: 126 | raise PagSeguroValidationError(error_messages['invalid']) 127 | 128 | return orig_value 129 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.11.1 2 | xmltodict>=0.10.2 3 | arrow>=0.8.0 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flake8==2.6.2 2 | pytest==2.9.2 3 | pytest-cov==2.3.0 4 | pytest-sugar==0.7.1 5 | responses==0.5.1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | readme = open('README.md').read() 9 | 10 | with open('requirements.txt') as reqs: 11 | requirements = reqs.read().split() 12 | 13 | 14 | setup( 15 | name='pagseguro', 16 | version='0.3.4', 17 | description='Pagseguro API v2 wrapper', 18 | author='Bruno Rocha', 19 | author_email='rochacbruno@gmail.com', 20 | url='https://github.com/rochacbruno/python-pagseguro', 21 | packages=['pagseguro', ], 22 | package_dir={'pagseguro': 'pagseguro'}, 23 | include_package_data=True, 24 | install_requires=requirements, 25 | long_description=readme, 26 | long_description_content_type='text/markdown', 27 | license='MIT', 28 | test_suite='tests', 29 | zip_safe=False, 30 | classifiers=[ 31 | 'Development Status :: 3 - Alpha', 32 | 'Environment :: Web Environment', 33 | 'Intended Audience :: Developers', 34 | 'Natural Language :: English', 35 | 'License :: OSI Approved :: BSD License', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.6', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.3', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | ], 45 | keywords='pagseguro, payment, payments, credit-card' 46 | ) 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Japle/python-pagseguro/08a8aa7f934b16d00948ead17a0e470a88f2479f/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from pagseguro import PagSeguro 5 | 6 | 7 | @pytest.fixture(scope='session') 8 | def sender(): 9 | return { 10 | 'name': u'Guybrush Treepwood', 11 | 'area_code': 11, 12 | "phone": 5555555, 13 | "email": 'guybrush@monkeyisland.com', 14 | "cpf": "00000000000", 15 | "born_date": "06/08/1650", 16 | } 17 | 18 | 19 | @pytest.fixture(scope='session') 20 | def shipping(): 21 | return { 22 | "type": PagSeguro.SEDEX, 23 | "street": "Av Brig Faria Lima", 24 | "number": 1234, 25 | "complement": "5 andar", 26 | "district": "Jardim Paulistano", 27 | "postal_code": "06650030", 28 | "city": "Sao Paulo", 29 | "state": "SP", 30 | "country": "BRA", 31 | "cost": "1234.56" 32 | } 33 | 34 | 35 | @pytest.fixture(scope='session') 36 | def items(): 37 | return [ 38 | { 39 | "id": "0001", 40 | "description": "Produto 1", 41 | "amount": 354.20, 42 | "quantity": 2, 43 | "weight": 200 44 | }, 45 | { 46 | "id": "0002", 47 | "description": "Produto 2", 48 | "amount": 355.20, 49 | "quantity": 1, 50 | "weight": 200 51 | }, 52 | ] 53 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from pagseguro.config import Config 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture 9 | def custom_config(): 10 | return { 11 | 'payment_url': 'http://google.com' 12 | } 13 | 14 | 15 | @pytest.fixture 16 | def common_config(): 17 | return { # flake8: noqa 18 | 'CHECKOUT_URL': 'https://ws.pagseguro.uol.com.br/v2/checkout', 19 | 'CURRENCY': 'BRL', 20 | 'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S', 21 | 'HEADERS': {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, 22 | 'NOTIFICATION_URL': 'https://ws.pagseguro.uol.com.br/v2/transactions/notifications/%s', 23 | 'PAYMENT_URL': 'https://pagseguro.uol.com.br/v2/checkout/payment.html?code=%s', 24 | 'PRE_APPROVAL_CANCEL_URL': 'https://ws.pagseguro.uol.com.br/v2/pre-approvals/cancel/%s', 25 | 'PRE_APPROVAL_NOTIFICATION_URL': 'https://ws.pagseguro.uol.com.br/v2/pre-approvals/notifications/%s', 26 | 'PRE_APPROVAL_PAYMENT_URL': 'https://ws.pagseguro.uol.com.br/v2/pre-approvals/payment', 27 | 'QUERY_PRE_APPROVAL_URL': 'https://ws.pagseguro.uol.com.br/v2/pre-approvals', 28 | 'QUERY_TRANSACTION_URL': 'https://ws.pagseguro.uol.com.br/v2/transactions', 29 | 'SESSION_CHECKOUT_URL': 'https://ws.pagseguro.uol.com.br/v2/sessions/', 30 | 'TRANSACTION_URL': 'https://ws.pagseguro.uol.com.br/v2/transactions/%s', 31 | 'TRANSPARENT_CHECKOUT_URL': 'https://ws.pagseguro.uol.com.br/v2/transactions' 32 | } 33 | 34 | 35 | def test_common_config(common_config): 36 | c = Config() 37 | for key, val in common_config.items(): 38 | print(key, val) 39 | assert getattr(c, key) == common_config[key] 40 | 41 | 42 | def test_custom_config(custom_config): 43 | c = Config(**custom_config) 44 | assert c.PAYMENT_URL == custom_config['payment_url'] 45 | 46 | def test_config_special_methods(): 47 | c = Config() 48 | assert c.__getitem__('PAYMENT_URL') == c.PAYMENT_URL 49 | 50 | c.__setitem__('PAYMENT_URL', 'http://google.com') 51 | assert c.PAYMENT_URL == 'http://google.com' 52 | assert c['PAYMENT_URL'] == 'http://google.com' 53 | -------------------------------------------------------------------------------- /tests/test_pagseguro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from pagseguro import PagSeguro, PagSeguroTransactionSearchResult 5 | from pagseguro.config import Config 6 | from pagseguro.exceptions import PagSeguroValidationError 7 | from pagseguro.utils import is_valid_email, is_valid_cpf 8 | 9 | TOKEN = '123456' 10 | EMAIL = 'seu@email.com' 11 | 12 | 13 | @pytest.fixture 14 | def pagseguro(): 15 | return PagSeguro(token=TOKEN, email=EMAIL) 16 | 17 | 18 | @pytest.fixture 19 | def xml(): 20 | return """ 21 | 22 | 2011-02-16T20:14:35.000-02:00 23 | 1 24 | 2 25 | 1 26 | 27 | 28 | 2011-02-05T15:46:12.000-02:00 29 | 2011-02-15T17:39:14.000-03:00 30 | 9E884542-81B3-4419-9A75-BCC6FB495EF1 31 | REF1234 32 | 1 33 | 3 34 | 35 | 1 36 | 37 | 49900.00 38 | 0.00 39 | 0.00 40 | 49900.00 41 | 0.00 42 | 43 | 44 | 2011-02-07T18:57:52.000-02:00 45 | 2011-02-14T21:37:24.000-03:00 46 | 2FB07A22-68FF-4F83-A356-24153A0C05E1 47 | REF5678 48 | 3 49 | 4 50 | 51 | 3 52 | 53 | 26900.00 54 | 0.00 55 | 0.00 56 | 26900.00 57 | 0.00 58 | 59 | 60 | """ 61 | 62 | 63 | def test_pagseguro_class(pagseguro): 64 | assert isinstance(pagseguro, PagSeguro) 65 | 66 | 67 | def test_pagseguro_initial_attrs(pagseguro): 68 | assert isinstance(pagseguro.config, Config) 69 | assert isinstance(pagseguro.data, dict) 70 | assert 'email' in pagseguro.data 71 | assert 'token' in pagseguro.data 72 | assert pagseguro.data['email'] == EMAIL 73 | assert pagseguro.data['token'] == TOKEN 74 | assert isinstance(pagseguro.items, list) 75 | assert isinstance(pagseguro.sender, dict) 76 | assert isinstance(pagseguro.shipping, dict) 77 | assert pagseguro._reference == '' 78 | assert pagseguro.extra_amount is None 79 | assert pagseguro.redirect_url is None 80 | assert pagseguro.notification_url is None 81 | assert pagseguro.abandon_url is None 82 | 83 | 84 | def test_build_checkout_params_with_all_params(pagseguro, sender, shipping, 85 | items): 86 | pagseguro.sender = sender 87 | pagseguro.shipping = shipping 88 | pagseguro.extra_amount = 12.50 89 | pagseguro.redirect_url = '/redirecionando/' 90 | pagseguro.abandon_url = '/abandonando/' 91 | pagseguro.items = items 92 | pagseguro.build_checkout_params() 93 | 94 | # check all data fields 95 | assert isinstance(pagseguro.data, dict) 96 | keys = ['email', 'token', 'senderName', 'senderAreaCode', 97 | 'senderPhone', 'senderEmail', 'senderCPF', 'senderBornDate', 98 | 'shippingType', 'shippingAddressStreet', 99 | 'shippingAddressNumber', 'shippingAddressComplement', 100 | 'shippingAddressDistrict', 'shippingAddressPostalCode', 101 | 'shippingAddressCity', 'shippingAddressState', 102 | 'shippingAddressCountry', 'shippingCost', 'extraAmount', 103 | 'redirectURL', 'abandonURL'] 104 | # items 105 | item_keys = ['itemId{}', 'itemDescription{}', 'itemAmount{}', 106 | 'itemQuantity{}', 'itemWeight{}'] 107 | 108 | for key in keys: 109 | assert key in pagseguro.data 110 | 111 | for i, key in enumerate(pagseguro.items, 1): 112 | keys_to_compare = map(lambda x: x.format(i), item_keys) 113 | for item_key in keys_to_compare: 114 | assert item_key in pagseguro.data 115 | 116 | 117 | def test_add_items_util(items): 118 | pagseguro = PagSeguro(token=TOKEN, email=EMAIL) 119 | pagseguro.add_item(**items[0]) 120 | pagseguro.add_item(**items[1]) 121 | assert len(pagseguro.items) == 2 122 | 123 | 124 | def test_reference(pagseguro): 125 | pagseguro.reference = '12345' 126 | assert str(pagseguro.reference) == u'REF12345' 127 | 128 | 129 | def test_clean_none_params(sender): 130 | pagseguro = PagSeguro(email=EMAIL, token=TOKEN) 131 | sender_copy = sender.copy() 132 | sender_copy['cpf'] = None 133 | sender_copy['born_date'] = None 134 | pagseguro.sender = sender_copy 135 | pagseguro.build_checkout_params() 136 | assert 'senderCPF' not in pagseguro.data 137 | assert 'senderBornData' not in pagseguro.data 138 | 139 | 140 | def test_is_valid_email(sender): 141 | bad_email = 'john.com' 142 | pagseguro = PagSeguro(email=bad_email, token=TOKEN) 143 | pagseguro.sender = {'email': bad_email} 144 | with pytest.raises(PagSeguroValidationError): 145 | pagseguro.build_checkout_params() 146 | 147 | # Now testing with a valid email 148 | pagseguro.sender['email'] = sender['email'] 149 | assert is_valid_email(pagseguro.sender['email']) == sender['email'] 150 | 151 | 152 | def test_is_valid_cpf(): 153 | bad_cpf = '123.456.267-45' 154 | pagseguro = PagSeguro(email=EMAIL, token=TOKEN) 155 | pagseguro.sender = {'cpf': bad_cpf} 156 | 157 | with pytest.raises(PagSeguroValidationError): 158 | pagseguro.build_checkout_params() 159 | 160 | # Now testing with a valid email 161 | pagseguro.sender['cpf'] = '482.268.465-28' 162 | assert is_valid_cpf(pagseguro.sender['cpf']) == pagseguro.sender['cpf'] 163 | 164 | pagseguro.sender['cpf'] = '48226846528' 165 | assert is_valid_cpf(pagseguro.sender['cpf']) == pagseguro.sender['cpf'] 166 | 167 | 168 | def test_parse_xml(xml): 169 | pg = PagSeguro(email=EMAIL, token=TOKEN) 170 | result = PagSeguroTransactionSearchResult(xml, pg.config) 171 | assert result.current_page == 1 172 | assert result.results_in_page == 2 173 | assert result.total_pages == 1 174 | assert len(result.transactions) == 2 175 | 176 | 177 | def test_pagseguro_with_bad_config(): 178 | with pytest.raises(Exception): 179 | PagSeguro(email=EMAIL, token=TOKEN, config=2) 180 | -------------------------------------------------------------------------------- /tests/test_pagseguro_sandbox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from pagseguro import PagSeguro, PagSeguroTransactionSearchResult 5 | from pagseguro.exceptions import PagSeguroValidationError 6 | from pagseguro.utils import is_valid_email, is_valid_cpf 7 | 8 | TOKEN = 'sandbox_token' 9 | EMAIL = 'pagseguro_email' 10 | 11 | 12 | @pytest.fixture 13 | def pagseguro_sandbox(): 14 | return PagSeguro(token=TOKEN, email=EMAIL, config=dict(sandbox=True)) 15 | 16 | 17 | @pytest.fixture 18 | def xml_sandbox(): 19 | return """ 20 | 21 | 2011-02-16T20:14:35.000-02:00 22 | 1 23 | 2 24 | 1 25 | 26 | 27 | 2011-02-05T15:46:12.000-02:00 28 | 2011-02-15T17:39:14.000-03:00 29 | 9E884542-81B3-4419-9A75-BCC6FB495EF1 30 | REF1234 31 | 1 32 | 3 33 | 34 | 1 35 | 36 | 49900.00 37 | 0.00 38 | 0.00 39 | 49900.00 40 | 0.00 41 | 42 | 43 | 2011-02-07T18:57:52.000-02:00 44 | 2011-02-14T21:37:24.000-03:00 45 | 2FB07A22-68FF-4F83-A356-24153A0C05E1 46 | REF5678 47 | 3 48 | 4 49 | 50 | 3 51 | 52 | 26900.00 53 | 0.00 54 | 0.00 55 | 26900.00 56 | 0.00 57 | 58 | 59 | """ 60 | 61 | 62 | def test_pagseguro_class(pagseguro_sandbox): 63 | assert isinstance(pagseguro_sandbox, PagSeguro) 64 | 65 | 66 | def test_pagseguro_initial_attrs(pagseguro_sandbox): 67 | assert isinstance(pagseguro_sandbox.data, dict) 68 | assert 'email' in pagseguro_sandbox.data 69 | assert 'token' in pagseguro_sandbox.data 70 | assert pagseguro_sandbox.data['email'] == EMAIL 71 | assert pagseguro_sandbox.data['token'] == TOKEN 72 | assert isinstance(pagseguro_sandbox.items, list) 73 | assert isinstance(pagseguro_sandbox.sender, dict) 74 | assert isinstance(pagseguro_sandbox.shipping, dict) 75 | assert pagseguro_sandbox._reference == '' 76 | assert pagseguro_sandbox.extra_amount is None 77 | assert pagseguro_sandbox.redirect_url is None 78 | assert pagseguro_sandbox.notification_url is None 79 | assert pagseguro_sandbox.abandon_url is None 80 | 81 | 82 | def test_build_checkout_params_with_all_params(pagseguro_sandbox, sender, 83 | shipping, items): 84 | pagseguro_sandbox.sender = sender 85 | pagseguro_sandbox.shipping = shipping 86 | pagseguro_sandbox.extra_amount = 12.50 87 | pagseguro_sandbox.redirect_url = '/redirecionando/' 88 | pagseguro_sandbox.abandon_url = '/abandonando/' 89 | pagseguro_sandbox.items = items 90 | pagseguro_sandbox.build_checkout_params() 91 | # check all data fields 92 | assert isinstance(pagseguro_sandbox.data, dict) 93 | keys = ['email', 'token', 'senderName', 'senderAreaCode', 94 | 'senderPhone', 'senderEmail', 'senderCPF', 'senderBornDate', 95 | 'shippingType', 'shippingAddressStreet', 96 | 'shippingAddressNumber', 'shippingAddressComplement', 97 | 'shippingAddressDistrict', 'shippingAddressPostalCode', 98 | 'shippingAddressCity', 'shippingAddressState', 99 | 'shippingAddressCountry', 'shippingCost', 'extraAmount', 100 | 'redirectURL', 'abandonURL'] 101 | 102 | # items 103 | item_keys = ['itemId{}', 'itemDescription{}', 'itemAmount{}', 104 | 'itemQuantity{}', 'itemWeight{}'] 105 | 106 | import pprint 107 | pprint.pprint(pagseguro_sandbox.data) 108 | 109 | for key in keys: 110 | assert key in pagseguro_sandbox.data 111 | 112 | for i, key in enumerate(pagseguro_sandbox.items, 1): 113 | keys_to_compare = map(lambda x: x.format(i), item_keys) 114 | for item_key in keys_to_compare: 115 | assert item_key in pagseguro_sandbox.data 116 | 117 | 118 | def test_add_items_util(items): 119 | pagseguro = PagSeguro(email=EMAIL, token=TOKEN) 120 | pagseguro.add_item(**items[0]) 121 | pagseguro.add_item(**items[1]) 122 | assert len(pagseguro.items) == 2 123 | 124 | 125 | def test_reference(pagseguro_sandbox): 126 | pagseguro_sandbox.reference = '12345' 127 | assert str(pagseguro_sandbox.reference) == u'REF12345' 128 | 129 | 130 | def test_clean_none_params(sender): 131 | pagseguro = PagSeguro(email=EMAIL, token=TOKEN) 132 | sender_copy = sender.copy() 133 | sender_copy['cpf'] = None 134 | sender_copy['born_date'] = None 135 | pagseguro.sender = sender_copy 136 | pagseguro.build_checkout_params() 137 | 138 | assert 'senderCPF' not in pagseguro.data 139 | assert 'senderBornData' not in pagseguro.data 140 | 141 | 142 | def test_is_valid_email(sender): 143 | bad_email = 'john.com' 144 | pagseguro = PagSeguro(email=bad_email, token=TOKEN) 145 | pagseguro.sender = {'email': bad_email} 146 | with pytest.raises(PagSeguroValidationError): 147 | pagseguro.build_checkout_params() 148 | 149 | # Now testing with a valid email 150 | pagseguro.sender['email'] = sender['email'] 151 | assert is_valid_email(pagseguro.sender['email']) == sender['email'] 152 | 153 | 154 | def test_is_valid_cpf(): 155 | bad_cpf = '123.456.267-45' 156 | pagseguro = PagSeguro(email=EMAIL, token=TOKEN) 157 | pagseguro.sender = {'cpf': bad_cpf} 158 | with pytest.raises(PagSeguroValidationError): 159 | pagseguro.build_checkout_params() 160 | 161 | # Now testing with a valid email 162 | pagseguro.sender['cpf'] = '482.268.465-28' 163 | assert is_valid_cpf(pagseguro.sender['cpf']) == pagseguro.sender['cpf'] 164 | 165 | pagseguro.sender['cpf'] = '48226846528' 166 | assert is_valid_cpf(pagseguro.sender['cpf']) == pagseguro.sender['cpf'] 167 | 168 | 169 | def test_parse_xml(xml_sandbox): 170 | pg = PagSeguro(email='seu@email.com', token='123456') 171 | result = PagSeguroTransactionSearchResult(xml_sandbox, pg.config) 172 | assert result.current_page == 1 173 | assert result.results_in_page == 2 174 | assert result.total_pages == 1 175 | assert len(result.transactions) == 2 176 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from pagseguro.utils import (is_valid_cpf, is_valid_cnpj, is_valid_email, 5 | parse_date) 6 | from pagseguro.exceptions import PagSeguroValidationError 7 | 8 | import pytest 9 | from dateutil.tz import tzutc 10 | 11 | 12 | def test_is_valid_email(): 13 | valid = 'test@email.com' 14 | valid2 = u'user@росси́я.ро' 15 | not_valid = '@asd.com' 16 | not_valid2 = 'bad' 17 | not_valid3 = u'user@росси́я' 18 | 19 | with pytest.raises(PagSeguroValidationError): 20 | is_valid_email(not_valid) 21 | 22 | with pytest.raises(PagSeguroValidationError): 23 | is_valid_email(not_valid2) 24 | 25 | with pytest.raises(PagSeguroValidationError): 26 | is_valid_email(not_valid3) 27 | 28 | assert is_valid_email(valid) == 'test@email.com' 29 | assert is_valid_email(valid2) == u'user@росси́я.ро' 30 | 31 | 32 | def test_parse_date(): 33 | # DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S' 34 | date_str = '2016-10-10T10:10:10' 35 | assert parse_date(date_str) == datetime.datetime(2016, 10, 10, 10, 10, 10, 36 | tzinfo=tzutc()) 37 | 38 | 39 | def test_is_valid_cpf(): 40 | valid = '041.684.826-50' 41 | valid2 = '04168482650' 42 | bad = 'bla///' 43 | max_digits = '1111111111111111111111111' 44 | invalid_cpf = '040.684.826-50' 45 | 46 | with pytest.raises(PagSeguroValidationError): 47 | is_valid_cpf(bad) 48 | 49 | with pytest.raises(PagSeguroValidationError): 50 | is_valid_cpf(max_digits) 51 | 52 | with pytest.raises(PagSeguroValidationError): 53 | is_valid_cpf(invalid_cpf) 54 | 55 | assert is_valid_cpf(valid) == valid 56 | assert is_valid_cpf(valid2) == '04168482650' 57 | 58 | 59 | def test_is_valid_cnpj(): 60 | valid = '31331052000174' 61 | valid2 = '72.168.117/0001-90' 62 | 63 | invalid = '///' 64 | digits = '1111111' 65 | wrong_number = '31331052000175' 66 | 67 | with pytest.raises(PagSeguroValidationError): 68 | is_valid_cnpj(invalid) 69 | 70 | with pytest.raises(PagSeguroValidationError): 71 | is_valid_cnpj(digits) 72 | 73 | with pytest.raises(PagSeguroValidationError): 74 | is_valid_cnpj(wrong_number) 75 | 76 | assert is_valid_cnpj(valid) == '31331052000174' 77 | assert is_valid_cnpj(valid2) == '72168117000190' 78 | --------------------------------------------------------------------------------