├── .github └── workflows │ └── django.yaml ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.rst ├── apps ├── __init__.py └── tickets │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── exceptions.py │ ├── forms.py │ ├── managers.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── templates │ └── tickets │ │ ├── base.html │ │ ├── cart_detail.html │ │ ├── event_detail.html │ │ ├── event_list.html │ │ ├── purchase_detail.html │ │ └── purchase_list.html │ ├── urls.py │ └── views.py ├── django_pagseguro2_example ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── local.env ├── manage.py ├── pytest.ini └── tests ├── __init__.py └── tickets ├── __init__.py ├── conftest.py ├── test_models.py └── test_views.py /.github/workflows/django.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 4 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.8 18 | - name: Install Dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pipenv 22 | pipenv install --dev 23 | - name: Run Tests 24 | run: | 25 | pipenv run pytest 26 | env: 27 | SECRET_KEY: my-secret-key 28 | DEBUG: true 29 | PAGSEGURO_EMAIL: fulano@email.com 30 | PAGSEGURO_TOKEN: token 31 | PAGSEGURO_SANDBOX: true 32 | PAGSEGURO_LOG_IN_MODEL: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # sqlite3 database 104 | *.sqlite3 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Allisson Azevedo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | [packages] 8 | 9 | django = "*" 10 | django-pagseguro2 = "*" 11 | prettyconf = "*" 12 | 13 | [dev-packages] 14 | 15 | codecov = "*" 16 | flake8 = "*" 17 | ipdb = "*" 18 | ipython = "*" 19 | pytest-cov = "*" 20 | pytest-django = "*" 21 | pytest-lazy-fixture = "*" 22 | python-status = "*" 23 | responses = "*" 24 | 25 | [requires] 26 | 27 | python_version = "3.8" 28 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "2e2cf4518c29ee5970492bda9d317570bf593e615bfab21e0d826228d5d25484" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5", 22 | "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c" 23 | ], 24 | "version": "==3.2.7" 25 | }, 26 | "certifi": { 27 | "hashes": [ 28 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 29 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 30 | ], 31 | "version": "==2020.4.5.1" 32 | }, 33 | "chardet": { 34 | "hashes": [ 35 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 36 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 37 | ], 38 | "version": "==3.0.4" 39 | }, 40 | "django": { 41 | "hashes": [ 42 | "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2", 43 | "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8" 44 | ], 45 | "index": "pypi", 46 | "version": "==3.0.7" 47 | }, 48 | "django-pagseguro2": { 49 | "hashes": [ 50 | "sha256:75551f9ff0c008df194afa0ba59439c30e2b2364a9a1458470863be37482379e", 51 | "sha256:ccc1f7a647f15a99de27683f39063417b2552354d6b230f7f0e04a3802bdeb5d" 52 | ], 53 | "index": "pypi", 54 | "version": "==3.0.0" 55 | }, 56 | "idna": { 57 | "hashes": [ 58 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 59 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 60 | ], 61 | "version": "==2.9" 62 | }, 63 | "prettyconf": { 64 | "hashes": [ 65 | "sha256:358af2f0d722999eaa4b3e0eb790fd10fad33bcb697d9bcacd53b58ff7fb342e" 66 | ], 67 | "index": "pypi", 68 | "version": "==2.1.0" 69 | }, 70 | "python-dateutil": { 71 | "hashes": [ 72 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 73 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 74 | ], 75 | "version": "==2.8.1" 76 | }, 77 | "pytz": { 78 | "hashes": [ 79 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 80 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 81 | ], 82 | "version": "==2020.1" 83 | }, 84 | "requests": { 85 | "hashes": [ 86 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 87 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 88 | ], 89 | "version": "==2.23.0" 90 | }, 91 | "six": { 92 | "hashes": [ 93 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 94 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 95 | ], 96 | "version": "==1.15.0" 97 | }, 98 | "sqlparse": { 99 | "hashes": [ 100 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", 101 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" 102 | ], 103 | "version": "==0.3.1" 104 | }, 105 | "urllib3": { 106 | "hashes": [ 107 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 108 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 109 | ], 110 | "version": "==1.25.9" 111 | }, 112 | "xmltodict": { 113 | "hashes": [ 114 | "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21", 115 | "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051" 116 | ], 117 | "version": "==0.12.0" 118 | } 119 | }, 120 | "develop": { 121 | "attrs": { 122 | "hashes": [ 123 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 124 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 125 | ], 126 | "version": "==19.3.0" 127 | }, 128 | "backcall": { 129 | "hashes": [ 130 | "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", 131 | "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" 132 | ], 133 | "version": "==0.1.0" 134 | }, 135 | "certifi": { 136 | "hashes": [ 137 | "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", 138 | "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" 139 | ], 140 | "version": "==2020.4.5.1" 141 | }, 142 | "chardet": { 143 | "hashes": [ 144 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 145 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 146 | ], 147 | "version": "==3.0.4" 148 | }, 149 | "codecov": { 150 | "hashes": [ 151 | "sha256:09fb045eb044a619cd2b9dacd7789ae8e322cb7f18196378579fd8d883e6b665", 152 | "sha256:aeeefa3a03cac8a78e4f988e935b51a4689bb1f17f20d4e827807ee11135f845" 153 | ], 154 | "index": "pypi", 155 | "version": "==2.0.22" 156 | }, 157 | "coverage": { 158 | "hashes": [ 159 | "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a", 160 | "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355", 161 | "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65", 162 | "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7", 163 | "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9", 164 | "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1", 165 | "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0", 166 | "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55", 167 | "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c", 168 | "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6", 169 | "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef", 170 | "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019", 171 | "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e", 172 | "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0", 173 | "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf", 174 | "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24", 175 | "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2", 176 | "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c", 177 | "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4", 178 | "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0", 179 | "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd", 180 | "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04", 181 | "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e", 182 | "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730", 183 | "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2", 184 | "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768", 185 | "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796", 186 | "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7", 187 | "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a", 188 | "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489", 189 | "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052" 190 | ], 191 | "version": "==5.1" 192 | }, 193 | "decorator": { 194 | "hashes": [ 195 | "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", 196 | "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" 197 | ], 198 | "version": "==4.4.2" 199 | }, 200 | "entrypoints": { 201 | "hashes": [ 202 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 203 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 204 | ], 205 | "version": "==0.3" 206 | }, 207 | "flake8": { 208 | "hashes": [ 209 | "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", 210 | "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" 211 | ], 212 | "index": "pypi", 213 | "version": "==3.7.9" 214 | }, 215 | "idna": { 216 | "hashes": [ 217 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 218 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 219 | ], 220 | "version": "==2.9" 221 | }, 222 | "ipdb": { 223 | "hashes": [ 224 | "sha256:77fb1c2a6fccdfee0136078c9ed6fe547ab00db00bebff181f1e8c9e13418d49" 225 | ], 226 | "index": "pypi", 227 | "version": "==0.13.2" 228 | }, 229 | "ipython": { 230 | "hashes": [ 231 | "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a", 232 | "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333" 233 | ], 234 | "index": "pypi", 235 | "version": "==7.13.0" 236 | }, 237 | "ipython-genutils": { 238 | "hashes": [ 239 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 240 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 241 | ], 242 | "version": "==0.2.0" 243 | }, 244 | "jedi": { 245 | "hashes": [ 246 | "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798", 247 | "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030" 248 | ], 249 | "version": "==0.17.0" 250 | }, 251 | "mccabe": { 252 | "hashes": [ 253 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 254 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 255 | ], 256 | "version": "==0.6.1" 257 | }, 258 | "more-itertools": { 259 | "hashes": [ 260 | "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", 261 | "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" 262 | ], 263 | "version": "==8.3.0" 264 | }, 265 | "packaging": { 266 | "hashes": [ 267 | "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", 268 | "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" 269 | ], 270 | "version": "==20.4" 271 | }, 272 | "parso": { 273 | "hashes": [ 274 | "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0", 275 | "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c" 276 | ], 277 | "version": "==0.7.0" 278 | }, 279 | "pexpect": { 280 | "hashes": [ 281 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 282 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 283 | ], 284 | "markers": "sys_platform != 'win32'", 285 | "version": "==4.8.0" 286 | }, 287 | "pickleshare": { 288 | "hashes": [ 289 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 290 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 291 | ], 292 | "version": "==0.7.5" 293 | }, 294 | "pluggy": { 295 | "hashes": [ 296 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 297 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 298 | ], 299 | "version": "==0.13.1" 300 | }, 301 | "prompt-toolkit": { 302 | "hashes": [ 303 | "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8", 304 | "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04" 305 | ], 306 | "version": "==3.0.5" 307 | }, 308 | "ptyprocess": { 309 | "hashes": [ 310 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 311 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 312 | ], 313 | "version": "==0.6.0" 314 | }, 315 | "py": { 316 | "hashes": [ 317 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 318 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 319 | ], 320 | "version": "==1.8.1" 321 | }, 322 | "pycodestyle": { 323 | "hashes": [ 324 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 325 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 326 | ], 327 | "version": "==2.5.0" 328 | }, 329 | "pyflakes": { 330 | "hashes": [ 331 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 332 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 333 | ], 334 | "version": "==2.1.1" 335 | }, 336 | "pygments": { 337 | "hashes": [ 338 | "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", 339 | "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" 340 | ], 341 | "version": "==2.6.1" 342 | }, 343 | "pyparsing": { 344 | "hashes": [ 345 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 346 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 347 | ], 348 | "version": "==2.4.7" 349 | }, 350 | "pytest": { 351 | "hashes": [ 352 | "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", 353 | "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" 354 | ], 355 | "version": "==5.4.3" 356 | }, 357 | "pytest-cov": { 358 | "hashes": [ 359 | "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", 360 | "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" 361 | ], 362 | "index": "pypi", 363 | "version": "==2.8.1" 364 | }, 365 | "pytest-django": { 366 | "hashes": [ 367 | "sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5", 368 | "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9" 369 | ], 370 | "index": "pypi", 371 | "version": "==3.9.0" 372 | }, 373 | "pytest-lazy-fixture": { 374 | "hashes": [ 375 | "sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac", 376 | "sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6" 377 | ], 378 | "index": "pypi", 379 | "version": "==0.6.3" 380 | }, 381 | "python-status": { 382 | "hashes": [ 383 | "sha256:4e9c824754e3669a4a6565fd4c3d6c0dd4130e779c541ad168e71110ebce3e53" 384 | ], 385 | "index": "pypi", 386 | "version": "==1.0.1" 387 | }, 388 | "requests": { 389 | "hashes": [ 390 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 391 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 392 | ], 393 | "version": "==2.23.0" 394 | }, 395 | "responses": { 396 | "hashes": [ 397 | "sha256:0474ce3c897fbcc1aef286117c93499882d5c440f06a805947e4b1cb5ab3d474", 398 | "sha256:f83613479a021e233e82d52ffb3e2e0e2836d24b0cc88a0fa31978789f78d0e5" 399 | ], 400 | "index": "pypi", 401 | "version": "==0.10.12" 402 | }, 403 | "six": { 404 | "hashes": [ 405 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 406 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 407 | ], 408 | "version": "==1.15.0" 409 | }, 410 | "traitlets": { 411 | "hashes": [ 412 | "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", 413 | "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" 414 | ], 415 | "version": "==4.3.3" 416 | }, 417 | "urllib3": { 418 | "hashes": [ 419 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 420 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 421 | ], 422 | "version": "==1.25.9" 423 | }, 424 | "wcwidth": { 425 | "hashes": [ 426 | "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6", 427 | "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830" 428 | ], 429 | "version": "==0.2.3" 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | django-pagseguro2-example 3 | ========================= 4 | 5 | |Github Build Status| 6 | 7 | ---- 8 | 9 | Exemplo de projeto usando o django-pagseguro2. 10 | 11 | 12 | Dependências para rodar o projeto 13 | --------------------------------- 14 | 15 | * Python 3.8+ 16 | * Pipenv 17 | * `Ngrok`_ (para receber as notificações do PagSeguro) 18 | 19 | .. _`Ngrok`: https://ngrok.com/ 20 | 21 | 22 | Como rodar o projeto 23 | -------------------- 24 | 25 | .. code:: shell 26 | 27 | pipenv install --dev 28 | cp local.env .env 29 | vim .env # edite as informações usando seus dados de sandbox do PagSeguro 30 | python manage.py migrate 31 | python manage.py createsuperuser 32 | python manage.py runserver 0.0.0.0:8000 33 | 34 | Abra o navegador no endereço http://localhost:8000/admin/ para fazer login no sistema e criar os eventos e tickets. 35 | 36 | Acesse o endereço http://localhost:8000/eventos/ para navegar pelos eventos e comprar os tickets. 37 | 38 | Como receber notificações do PagSeguro 39 | -------------------------------------- 40 | 41 | .. code:: shell 42 | 43 | ngrok http 8000 44 | 45 | Anote o endereço do ngrok e atualize no `sandbox do PagSeguro`_. Ex: https://14742c67.ngrok.io/pagseguro/ (observe o '/' no final da url). 46 | 47 | No sandbox, altere uma transação para o status pago e a notificação será enviada para o sistema. 48 | 49 | .. _`sandbox do PagSeguro`: https://sandbox.pagseguro.uol.com.br/vendedor/configuracoes.html 50 | 51 | 52 | Como rodar os testes 53 | -------------------- 54 | 55 | .. code:: shell 56 | 57 | pytest 58 | 59 | 60 | Observações 61 | ----------- 62 | 63 | * Todos os endereços /eventos/* são protegidos por login e senha, lembre-se de logar no admin antes de acessar. 64 | * Apenas os status pago e cancelado que vem do PagSeguro foram mapeados nesse projeto. 65 | 66 | .. |Github Build Status| image:: https://github.com/allisson/django-pagseguro2-example/workflows/tests/badge.svg 67 | :target: https://github.com/allisson/django-pagseguro2-example/actions 68 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/apps/__init__.py -------------------------------------------------------------------------------- /apps/tickets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/apps/tickets/__init__.py -------------------------------------------------------------------------------- /apps/tickets/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Event, Ticket, Cart, CartItem, Purchase 4 | 5 | 6 | class TicketInline(admin.TabularInline): 7 | model = Ticket 8 | 9 | 10 | class EventAdmin(admin.ModelAdmin): 11 | list_display = ('id', 'title', 'description', 'created_at') 12 | inlines = [ 13 | TicketInline, 14 | ] 15 | 16 | 17 | class CartItemInline(admin.TabularInline): 18 | model = CartItem 19 | 20 | 21 | class CartAdmin(admin.ModelAdmin): 22 | list_display = ('id', 'user', 'closed', 'created_at') 23 | inlines = [ 24 | CartItemInline, 25 | ] 26 | 27 | 28 | class PurchaseAdmin(admin.ModelAdmin): 29 | list_display = ('id', 'user', 'cart', 'price', 'status', 'created_at') 30 | 31 | 32 | admin.site.register(Event, EventAdmin) 33 | admin.site.register(Cart, CartAdmin) 34 | admin.site.register(Purchase, PurchaseAdmin) 35 | -------------------------------------------------------------------------------- /apps/tickets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TicketsConfig(AppConfig): 5 | name = 'tickets' 6 | -------------------------------------------------------------------------------- /apps/tickets/exceptions.py: -------------------------------------------------------------------------------- 1 | class CheckoutException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /apps/tickets/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | 3 | from .models import CartItem 4 | 5 | 6 | class CartItemForm(ModelForm): 7 | class Meta: 8 | model = CartItem 9 | fields = ('ticket', 'quantity') 10 | -------------------------------------------------------------------------------- /apps/tickets/managers.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Manager 2 | 3 | from pagseguro.api import PagSeguroItem, PagSeguroApi 4 | 5 | from .exceptions import CheckoutException 6 | 7 | 8 | class CartManager(Manager): 9 | def get_cart_for_user(self, user): 10 | cart = self.filter(user=user, closed=False).first() 11 | if not cart: 12 | self.create(user=user, closed=False) 13 | return cart 14 | 15 | def add_cart_item(self, cart, ticket, quantity): 16 | cart_item = cart.cart_items.filter(cart=cart, ticket=ticket).first() 17 | if cart_item: 18 | cart_item.quantity += quantity 19 | cart_item.unit_price = ticket.price 20 | cart_item.save() 21 | return cart_item 22 | return cart.cart_items.create(cart=cart, ticket=ticket, quantity=quantity, unit_price=ticket.price) 23 | 24 | 25 | class PurchaseManager(Manager): 26 | def create_purchase(self, cart): 27 | return self.create(user=cart.user, cart=cart, price=cart.price) 28 | 29 | def create_checkout(self, cart): 30 | purchase = self.create_purchase(cart) 31 | pagseguro_api = PagSeguroApi(reference=str(purchase.id)) 32 | for cart_item in cart.cart_items.all(): 33 | ticket = cart_item.ticket 34 | item = PagSeguroItem( 35 | id=str(ticket.id), 36 | description=ticket.title, 37 | amount=str(cart_item.unit_price), 38 | quantity=cart_item.quantity 39 | ) 40 | pagseguro_api.add_item(item) 41 | pagseguro_data = pagseguro_api.checkout() 42 | if pagseguro_data['success'] is False: 43 | raise CheckoutException(pagseguro_data['message']) 44 | purchase.pagseguro_redirect_url = pagseguro_data['redirect_url'] 45 | purchase.save() 46 | cart.closed = True 47 | cart.save() 48 | return purchase 49 | 50 | def update_purchase_status(self, pagseguro_transaction): 51 | status_map = { 52 | '3': 'paid', 53 | '7': 'canceled' 54 | } 55 | purchase = self.filter(id=pagseguro_transaction['reference']).first() 56 | if not purchase: 57 | return 58 | if pagseguro_transaction['status'] not in ('3', '7'): 59 | return purchase 60 | purchase.status = status_map[pagseguro_transaction['status']] 61 | purchase.save() 62 | return purchase 63 | -------------------------------------------------------------------------------- /apps/tickets/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import apps.tickets.models 2 | from django.conf import settings 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ] 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Cart', 15 | fields=[ 16 | ('id', models.UUIDField(default=apps.tickets.models.generate_code, editable=False, primary_key=True, serialize=False)), 17 | ('created_at', models.DateTimeField(auto_now_add=True)), 18 | ('updated_at', models.DateTimeField(auto_now=True)), 19 | ('closed', models.BooleanField(db_index=True, default=False, verbose_name='carrinho finalizado')), 20 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='carts', to=settings.AUTH_USER_MODEL, verbose_name='usuário')), 21 | ], 22 | options={ 23 | 'verbose_name': 'carrinho de compra', 24 | 'verbose_name_plural': 'carrinhos de compra', 25 | 'ordering': ['-created_at'], 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='CartItem', 30 | fields=[ 31 | ('id', models.UUIDField(default=apps.tickets.models.generate_code, editable=False, primary_key=True, serialize=False)), 32 | ('created_at', models.DateTimeField(auto_now_add=True)), 33 | ('updated_at', models.DateTimeField(auto_now=True)), 34 | ('quantity', models.SmallIntegerField(default=1, verbose_name='quantidade')), 35 | ('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='preço unitário')), 36 | ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='tickets.Cart', verbose_name='carrinho')), 37 | ], 38 | options={ 39 | 'verbose_name': 'item do carrinho de compra', 40 | 'verbose_name_plural': 'itens do carrinho de compra', 41 | 'ordering': ['id'], 42 | }, 43 | ), 44 | migrations.CreateModel( 45 | name='Event', 46 | fields=[ 47 | ('id', models.UUIDField(default=apps.tickets.models.generate_code, editable=False, primary_key=True, serialize=False)), 48 | ('created_at', models.DateTimeField(auto_now_add=True)), 49 | ('updated_at', models.DateTimeField(auto_now=True)), 50 | ('title', models.CharField(max_length=128, verbose_name='título')), 51 | ('description', models.TextField(verbose_name='descrição')), 52 | ], 53 | options={ 54 | 'verbose_name': 'evento', 55 | 'verbose_name_plural': 'eventos', 56 | 'ordering': ['title'], 57 | }, 58 | ), 59 | migrations.CreateModel( 60 | name='Purchase', 61 | fields=[ 62 | ('id', models.UUIDField(default=apps.tickets.models.generate_code, editable=False, primary_key=True, serialize=False)), 63 | ('created_at', models.DateTimeField(auto_now_add=True)), 64 | ('updated_at', models.DateTimeField(auto_now=True)), 65 | ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='preço')), 66 | ('status', models.CharField(choices=[('pending', 'Pendente'), ('paid', 'Pago'), ('canceled', 'Cancelado')], default='pending', max_length=16, verbose_name='status da compra')), 67 | ('pagseguro_redirect_url', models.URLField(blank=True, max_length=255, verbose_name='url do pagseguro')), 68 | ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchases', to='tickets.Cart', verbose_name='carrinho')), 69 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchases', to=settings.AUTH_USER_MODEL, verbose_name='usuário')), 70 | ], 71 | options={ 72 | 'verbose_name': 'compra', 73 | 'verbose_name_plural': 'compras', 74 | 'ordering': ['-created_at'], 75 | }, 76 | ), 77 | migrations.CreateModel( 78 | name='Ticket', 79 | fields=[ 80 | ('id', models.UUIDField(default=apps.tickets.models.generate_code, editable=False, primary_key=True, serialize=False)), 81 | ('created_at', models.DateTimeField(auto_now_add=True)), 82 | ('updated_at', models.DateTimeField(auto_now=True)), 83 | ('title', models.CharField(max_length=128, verbose_name='título')), 84 | ('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='preço')), 85 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='tickets.Event', verbose_name='evento')), 86 | ], 87 | options={ 88 | 'verbose_name': 'ticket', 89 | 'verbose_name_plural': 'tickets', 90 | 'ordering': ['title'], 91 | }, 92 | ), 93 | migrations.AddField( 94 | model_name='cartitem', 95 | name='ticket', 96 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='tickets.Ticket', verbose_name='ticket'), 97 | ), 98 | ] 99 | -------------------------------------------------------------------------------- /apps/tickets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/apps/tickets/migrations/__init__.py -------------------------------------------------------------------------------- /apps/tickets/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from pagseguro.signals import notificacao_recebida 6 | 7 | from .managers import CartManager, PurchaseManager 8 | 9 | 10 | def generate_code(): 11 | return uuid.uuid4() 12 | 13 | 14 | class BaseModel(models.Model): 15 | id = models.UUIDField(primary_key=True, editable=False, default=generate_code) 16 | created_at = models.DateTimeField(auto_now_add=True) 17 | updated_at = models.DateTimeField(auto_now=True) 18 | 19 | class Meta: 20 | abstract = True 21 | 22 | 23 | class Event(BaseModel): 24 | title = models.CharField('título', max_length=128) 25 | description = models.TextField('descrição') 26 | 27 | def __str__(self): 28 | return self.title 29 | 30 | class Meta: 31 | ordering = ['title'] 32 | verbose_name = 'evento' 33 | verbose_name_plural = 'eventos' 34 | 35 | 36 | class Ticket(BaseModel): 37 | event = models.ForeignKey('Event', on_delete=models.CASCADE, verbose_name='evento', related_name='tickets') 38 | title = models.CharField('título', max_length=128) 39 | price = models.DecimalField('preço', max_digits=10, decimal_places=2) 40 | 41 | def __str__(self): 42 | return self.title 43 | 44 | class Meta: 45 | ordering = ['title'] 46 | verbose_name = 'ticket' 47 | verbose_name_plural = 'tickets' 48 | 49 | 50 | class Cart(BaseModel): 51 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='usuário', related_name='carts') 52 | closed = models.BooleanField('carrinho finalizado', db_index=True, default=False) 53 | objects = CartManager() 54 | 55 | def __str__(self): 56 | return str(self.id) 57 | 58 | class Meta: 59 | ordering = ['-created_at'] 60 | verbose_name = 'carrinho de compra' 61 | verbose_name_plural = 'carrinhos de compra' 62 | 63 | @property 64 | def price(self): 65 | return sum([cart_item.price for cart_item in self.cart_items.all()]) 66 | 67 | 68 | class CartItem(BaseModel): 69 | cart = models.ForeignKey('Cart', on_delete=models.CASCADE, verbose_name='carrinho', related_name='cart_items') 70 | ticket = models.ForeignKey('Ticket', on_delete=models.CASCADE, verbose_name='ticket', related_name='cart_items') 71 | quantity = models.SmallIntegerField('quantidade', default=1) 72 | unit_price = models.DecimalField('preço unitário', max_digits=10, decimal_places=2) 73 | 74 | def __str__(self): 75 | return '{} - {} - {}'.format(self.cart, self.ticket, self.price) 76 | 77 | class Meta: 78 | ordering = ['id'] 79 | verbose_name = 'item do carrinho de compra' 80 | verbose_name_plural = 'itens do carrinho de compra' 81 | unique_together = ('cart', 'ticket') 82 | 83 | @property 84 | def price(self): 85 | return self.quantity * self.unit_price 86 | 87 | 88 | PURCHASE_STATUS_CHOICES = ( 89 | ('pending', 'Pendente'), 90 | ('paid', 'Pago'), 91 | ('canceled', 'Cancelado'), 92 | ) 93 | 94 | 95 | class Purchase(BaseModel): 96 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='usuário', related_name='purchases') 97 | cart = models.ForeignKey('Cart', on_delete=models.CASCADE, verbose_name='carrinho', related_name='purchases') 98 | price = models.DecimalField('preço', max_digits=10, decimal_places=2) 99 | status = models.CharField('status da compra', max_length=16, default='pending', choices=PURCHASE_STATUS_CHOICES) 100 | pagseguro_redirect_url = models.URLField('url do pagseguro', max_length=255, blank=True) 101 | objects = PurchaseManager() 102 | 103 | def __str__(self): 104 | return str(self.id) 105 | 106 | class Meta: 107 | ordering = ['-created_at'] 108 | verbose_name = 'compra' 109 | verbose_name_plural = 'compras' 110 | 111 | 112 | def update_purchase_status(sender, transaction, **kwargs): 113 | Purchase.objects.update_purchase_status(transaction) 114 | 115 | 116 | notificacao_recebida.connect(update_purchase_status) 117 | -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | 6 | 7 | 8 | 9 | 10 | {% block extra_head %}{% endblock %} 11 | 12 | 13 | 32 |
33 | {% block body %}{% endblock %} 34 | 35 | 36 | 37 | 38 | {% block extra_body %}{% endblock %} 39 | 40 | -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/cart_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'tickets/base.html' %} 2 | {% load humanize %} 3 | 4 | {% block body %} 5 |
6 |
7 |
8 |

Carrinho de compras

9 |

{{ event.description }}

10 |
11 |
12 |
13 | {% with cart_items=cart.cart_items.all %} 14 | {% if cart_items %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for cart_item in cart_items %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
ItemQuantidadePreço unitárioPreço
{{ cart_item.ticket.title }}{{ cart_item.quantity }}{{ cart_item.unit_price|intcomma }}{{ cart_item.price|intcomma }}
35 | Total: {{ cart.price|intcomma }} 36 |
37 |
38 | {% csrf_token %} 39 | 40 | 41 |
42 | {% else %} 43 | Carrinho vazio. 44 | {% endif %} 45 | {% endwith %} 46 | 47 |
48 | {% endblock %} 49 | 50 | {% block extra_body %} 51 | 62 | 63 | {% endblock %} -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/event_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'tickets/base.html' %} 2 | {% load humanize %} 3 | 4 | {% block title %}Evento: {{ event.title }}{% endblock %} 5 | 6 | {% block body %} 7 |
8 |
9 |
10 |

{{ event.title }}

11 |

{{ event.description }}

12 |
13 |
14 |
15 |
16 | {% for ticket in event.tickets.all %} 17 |
18 |
19 |
20 |
{{ ticket.title }}
21 |

Preço: {{ ticket.price|intcomma }}

22 |
23 | {% csrf_token %} 24 | 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | {% endfor %} 40 |
41 |
42 | {% endblock %} -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/event_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'tickets/base.html' %} 2 | 3 | {% block title %}Lista de eventos{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 |
9 |

Eventos

10 |

Selecione o evento para verificar os tickets disponíveis.

11 |
12 |
13 |
14 |
15 | {% for event in events %} 16 |
17 |
18 |
19 |
{{ event.title }}
20 |

{{ event.description }}

21 | Ver Tickets 22 |
23 |
24 |
25 |
26 | {% endfor %} 27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/purchase_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'tickets/base.html' %} 2 | {% load humanize %} 3 | 4 | {% block title %}Compra: {{ purchase.id }} {% endblock %} 5 | 6 | {% block body %} 7 |
8 |
9 |
10 |

Compra {{ purchase.id}}

11 |
12 |
13 |
14 |

Id: {{ purchase.id }}

15 |

Preço: {{ purchase.price|intcomma }}

16 |

Status: {{ purchase.get_status_display }}

17 | {% if purchase.status == 'pending' %} 18 |

Pagar

19 | {% endif %} 20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /apps/tickets/templates/tickets/purchase_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'tickets/base.html' %} 2 | {% load humanize %} 3 | 4 | {% block title %}Minhas compras{% endblock %} 5 | 6 | {% block body %} 7 |
8 |
9 |
10 |

Minhas compras

11 |
12 |
13 |
14 | {% if purchases %} 15 | {% for purchase in purchases %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for purchase in purchases %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
IdPreçoStatusData
{{ purchase.id }}{{ purchase.price|intcomma }}{{ purchase.status }}{{ purchase.created_at }}
36 | {% endfor %} 37 | {% else %} 38 | Você ainda não comprou nada. 39 | {% endif %} 40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /apps/tickets/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'tickets' 6 | urlpatterns = [ 7 | path('eventos/', views.event_list, name='event_list'), 8 | path('eventos//', views.event_detail, name='event_detail'), 9 | path('carrinho/', views.cart_detail, name='cart_detail'), 10 | path('carrinho/limpar/', views.cart_clear, name='cart_clear'), 11 | path('carrinho/adicionar/', views.cart_add_item, name='cart_add_item'), 12 | path('compras/criar/', views.purchase_create, name='purchase_create'), 13 | path('compras/', views.purchase_list, name='purchase_list'), 14 | path('compras//', views.purchase_detail, name='purchase_detail'), 15 | ] 16 | -------------------------------------------------------------------------------- /apps/tickets/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.db import transaction 3 | from django.shortcuts import render, get_object_or_404, redirect 4 | from django.urls import reverse 5 | from django.views.decorators.http import require_http_methods 6 | 7 | from .forms import CartItemForm 8 | from .models import Event, Cart, Purchase 9 | 10 | 11 | @login_required 12 | def event_list(request): 13 | events = Event.objects.all() 14 | context = { 15 | 'events': events, 16 | 'cart': Cart.objects.get_cart_for_user(request.user) 17 | } 18 | return render(request, 'tickets/event_list.html', context=context) 19 | 20 | 21 | @login_required 22 | def event_detail(request, id): 23 | event = get_object_or_404(Event, id=id) 24 | context = { 25 | 'event': event, 26 | 'cart': Cart.objects.get_cart_for_user(request.user) 27 | } 28 | return render(request, 'tickets/event_detail.html', context=context) 29 | 30 | 31 | @login_required 32 | def cart_detail(request): 33 | context = { 34 | 'cart': Cart.objects.get_cart_for_user(request.user) 35 | } 36 | return render(request, 'tickets/cart_detail.html', context=context) 37 | 38 | 39 | @login_required 40 | @require_http_methods(['POST']) 41 | def cart_clear(request): 42 | cart = Cart.objects.get_cart_for_user(request.user) 43 | cart.cart_items.all().delete() 44 | return redirect(reverse('tickets:cart_detail')) 45 | 46 | 47 | @login_required 48 | @require_http_methods(['POST']) 49 | def cart_add_item(request): 50 | cart = Cart.objects.get_cart_for_user(request.user) 51 | form = CartItemForm(request.POST) 52 | if form.is_valid(): 53 | ticket = form.cleaned_data.get('ticket') 54 | quantity = form.cleaned_data.get('quantity') 55 | Cart.objects.add_cart_item(cart, ticket, quantity) 56 | return redirect(reverse('tickets:cart_detail')) 57 | 58 | 59 | @login_required 60 | @require_http_methods(['POST']) 61 | @transaction.atomic 62 | def purchase_create(request): 63 | cart = Cart.objects.get_cart_for_user(request.user) 64 | purchase = Purchase.objects.create_checkout(cart) 65 | return redirect(reverse('tickets:purchase_detail', args=[purchase.id])) 66 | 67 | 68 | @login_required 69 | def purchase_list(request): 70 | purchases = Purchase.objects.filter(user=request.user) 71 | context = {'purchases': purchases} 72 | return render(request, 'tickets/purchase_list.html', context=context) 73 | 74 | 75 | @login_required 76 | def purchase_detail(request, id): 77 | purchase = get_object_or_404(Purchase, id=id, user=request.user) 78 | context = {'purchase': purchase} 79 | return render(request, 'tickets/purchase_detail.html', context=context) 80 | -------------------------------------------------------------------------------- /django_pagseguro2_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/django_pagseguro2_example/__init__.py -------------------------------------------------------------------------------- /django_pagseguro2_example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from prettyconf import config 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | SECRET_KEY = config('SECRET_KEY') 7 | 8 | DEBUG = config('DEBUG', cast=config.boolean) 9 | 10 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=config.list) 11 | 12 | INSTALLED_APPS = [ 13 | 'django.contrib.admin', 14 | 'django.contrib.auth', 15 | 'django.contrib.contenttypes', 16 | 'django.contrib.sessions', 17 | 'django.contrib.messages', 18 | 'django.contrib.staticfiles', 19 | 'django.contrib.humanize', 20 | 'pagseguro', 21 | 'apps.tickets', 22 | ] 23 | 24 | MIDDLEWARE = [ 25 | 'django.middleware.security.SecurityMiddleware', 26 | 'django.contrib.sessions.middleware.SessionMiddleware', 27 | 'django.middleware.common.CommonMiddleware', 28 | 'django.middleware.csrf.CsrfViewMiddleware', 29 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 30 | 'django.contrib.messages.middleware.MessageMiddleware', 31 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 32 | ] 33 | 34 | ROOT_URLCONF = 'django_pagseguro2_example.urls' 35 | 36 | TEMPLATES = [ 37 | { 38 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 39 | 'DIRS': [], 40 | 'APP_DIRS': True, 41 | 'OPTIONS': { 42 | 'context_processors': [ 43 | 'django.template.context_processors.debug', 44 | 'django.template.context_processors.request', 45 | 'django.contrib.auth.context_processors.auth', 46 | 'django.contrib.messages.context_processors.messages', 47 | ], 48 | }, 49 | }, 50 | ] 51 | 52 | WSGI_APPLICATION = 'django_pagseguro2_example.wsgi.application' 53 | 54 | DATABASES = { 55 | 'default': { 56 | 'ENGINE': 'django.db.backends.sqlite3', 57 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 58 | } 59 | } 60 | 61 | AUTH_PASSWORD_VALIDATORS = [ 62 | { 63 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 64 | }, 65 | { 66 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 67 | }, 68 | { 69 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 70 | }, 71 | { 72 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 73 | }, 74 | ] 75 | 76 | LANGUAGE_CODE = 'pt-br' 77 | 78 | TIME_ZONE = 'America/Sao_Paulo' 79 | 80 | USE_I18N = True 81 | 82 | USE_L10N = True 83 | 84 | USE_TZ = True 85 | 86 | STATIC_URL = '/static/' 87 | 88 | # Pagseguro 89 | PAGSEGURO_EMAIL = config('PAGSEGURO_EMAIL') 90 | 91 | PAGSEGURO_TOKEN = config('PAGSEGURO_TOKEN') 92 | 93 | PAGSEGURO_SANDBOX = config('PAGSEGURO_SANDBOX', cast=config.boolean) 94 | 95 | PAGSEGURO_LOG_IN_MODEL = config('PAGSEGURO_LOG_IN_MODEL', cast=config.boolean) 96 | -------------------------------------------------------------------------------- /django_pagseguro2_example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('apps.tickets.urls', namespace='tickets')), 7 | path('pagseguro/', include('pagseguro.urls')), 8 | ] 9 | -------------------------------------------------------------------------------- /django_pagseguro2_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_pagseguro2_example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_pagseguro2_example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /local.env: -------------------------------------------------------------------------------- 1 | SECRET_KEY=my-secret-key 2 | DEBUG=true 3 | PAGSEGURO_EMAIL=fulano@cicrano.com 4 | PAGSEGURO_TOKEN=token 5 | PAGSEGURO_SANDBOX=true 6 | PAGSEGURO_LOG_IN_MODEL=true 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_pagseguro2_example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=django_pagseguro2_example.settings 3 | addopts = --reuse-db --tb=native --cov=apps --cov-report=term-missing -vvv 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/tests/__init__.py -------------------------------------------------------------------------------- /tests/tickets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/django-pagseguro2-example/52f17aada102484c78367e40635950f67d69b05a/tests/tickets/__init__.py -------------------------------------------------------------------------------- /tests/tickets/conftest.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | import pytest 4 | 5 | from apps.tickets.models import Event, Ticket, Cart, CartItem, Purchase 6 | 7 | 8 | @pytest.fixture 9 | def event(): 10 | return Event.objects.create(title='My Event Title', description='My Event Description') 11 | 12 | 13 | @pytest.fixture 14 | def ticket(event): 15 | return Ticket.objects.create(event=event, title='My Ticket Title', price=Decimal('100.00')) 16 | 17 | 18 | @pytest.fixture 19 | def user(admin_user): 20 | return admin_user 21 | 22 | 23 | @pytest.fixture 24 | def cart(user): 25 | return Cart.objects.create(user=user, closed=False) 26 | 27 | 28 | @pytest.fixture 29 | def cart_item(cart, ticket): 30 | return CartItem.objects.create(cart=cart, ticket=ticket, quantity=1, unit_price=Decimal('100.00')) 31 | 32 | 33 | @pytest.fixture 34 | def purchase(cart_item): 35 | cart = cart_item.cart 36 | return Purchase.objects.create(user=cart.user, cart=cart, price=cart.price) 37 | 38 | 39 | @pytest.fixture 40 | def pagseguro_checkout_response(): 41 | return ''' 42 | 43 | 36BCD3B352526D1EE4D6FFA359934D7E 44 | 2018-01-27T10:11:28.000-02:00 45 | 46 | ''' 47 | 48 | 49 | @pytest.fixture 50 | def pagseguro_checkout_error_response(): 51 | return ''' 52 | 53 | 54 | 11029 55 | Some Error 56 | 57 | 58 | ''' 59 | 60 | 61 | @pytest.fixture 62 | def pagseguro_notification(): 63 | return { 64 | 'notificationCode': 'E015A4E1F0D1F0D1594114997F98ED9736BA', 65 | 'notificationType': 'transaction' 66 | } 67 | 68 | 69 | @pytest.fixture 70 | def pagseguro_transaction_response(): 71 | return ''' 72 | 73 | 2018-01-29T12:27:57.000-02:00 74 | 5479CBCF-2E8F-4594-A5E5-4BF2038C98C1 75 | 9fd05f66-315c-49f9-85f7-c92775f5a54d 76 | 1 77 | 1 78 | 2018-01-29T12:34:05.000-02:00 79 | 80 | 1 81 | 101 82 | 83 | 700.00 84 | 0.00 85 | 35.33 86 | 664.67 87 | 0.00 88 | 2018-01-29T12:34:05.000-02:00 89 | 1 90 | 2 91 | 92 | 93 | 4ea9bfb9-98f0-498a-9e05-02028358f9c1 94 | Ticket 2 95 | 2 96 | 200.00 97 | 98 | 99 | fd7e1175-c50b-45b6-92b7-5e12bf550c8f 100 | Ticket 3 101 | 1 102 | 300.00 103 | 104 | 105 | 106 | Comprador Virtual 107 | c11004631206281776849@sandbox.pagseguro.com.br 108 | 109 | 11 110 | 999999999 111 | 112 | 113 | 114 | CPF 115 | 59586852873 116 | 117 | 118 | 119 | 120 |
121 | RUA JOSE BRANCO RIBEIRO 122 | 840 123 | 124 | Catolé 125 | CAMPINA GRANDE 126 | PB 127 | BRA 128 | 58410175 129 |
130 | 3 131 | 0.00 132 |
133 | 134 | cielo 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 0 142 | 1056784170 143 | CIELO 144 | 145 |
146 | ''' 147 | 148 | 149 | @pytest.fixture 150 | def pagseguro_transaction(): 151 | return { 152 | 'date': '2018-01-29T12:27:57.000-02:00', 153 | 'code': '5479CBCF-2E8F-4594-A5E5-4BF2038C98C1', 154 | 'reference': '9fd05f66-315c-49f9-85f7-c92775f5a54d', 155 | 'type': '1', 156 | 'status': '1', 157 | 'lastEventDate': '2018-01-29T12:34:05.000-02:00', 158 | 'paymentMethod': { 159 | 'type': '1', 160 | 'code': '101' 161 | }, 162 | 'grossAmount': '700.00', 163 | 'discountAmount': '0.00', 164 | 'feeAmount': '35.33', 165 | 'netAmount': '664.67', 166 | 'extraAmount': '0.00', 167 | 'escrowEndDate': '2018-01-29T12:34:05.000-02:00', 168 | 'installmentCount': '1', 169 | 'itemCount': '2', 170 | 'items': { 171 | 'item': [ 172 | { 173 | 'id': '4ea9bfb9-98f0-498a-9e05-02028358f9c1', 174 | 'description': 'Ticket 2', 175 | 'quantity': '2', 176 | 'amount': '200.00' 177 | }, 178 | { 179 | 'id': 'fd7e1175-c50b-45b6-92b7-5e12bf550c8f', 180 | 'description': 'Ticket 3', 181 | 'quantity': '1', 182 | 'amount': '300.00' 183 | } 184 | ] 185 | }, 186 | 'sender': { 187 | 'name': 'Comprador Virtual', 188 | 'email': 'c11004631206281776849@sandbox.pagseguro.com.br', 189 | 'phone': { 190 | 'areaCode': '11', 191 | 'number': '999999999' 192 | }, 193 | 'documents': { 194 | 'document': { 195 | 'type': 'CPF', 196 | 'value': '59586852873' 197 | } 198 | } 199 | }, 200 | 'shipping': { 201 | 'address': { 202 | 'street': 'RUA JOSE BRANCO RIBEIRO', 203 | 'number': '840', 204 | 'complement': None, 205 | 'district': 'Catol\u00e9', 206 | 'city': 'CAMPINA GRANDE', 207 | 'state': 'PB', 208 | 'country': 'BRA', 209 | 'postalCode': '58410175' 210 | }, 211 | 'type': '3', 212 | 'cost': '0.00' 213 | }, 214 | 'gatewaySystem': { 215 | 'type': 'cielo', 216 | 'rawCode': { 217 | '@xsi:nil': 'true', 218 | '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' 219 | }, 220 | 'rawMessage': { 221 | '@xsi:nil': 'true', 222 | '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' 223 | }, 224 | 'normalizedCode': { 225 | '@xsi:nil': 'true', 226 | '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' 227 | }, 228 | 'normalizedMessage': { 229 | '@xsi:nil': 'true', 230 | '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' 231 | }, 232 | 'authorizationCode': '0', 233 | 'nsu': '0', 234 | 'tid': '0', 235 | 'establishmentCode': '1056784170', 236 | 'acquirerName': 'CIELO' 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /tests/tickets/test_models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from decimal import Decimal 3 | 4 | import pytest 5 | import responses 6 | from django.urls import reverse 7 | 8 | from apps.tickets.exceptions import CheckoutException 9 | from apps.tickets.models import Cart, CartItem, Purchase 10 | 11 | pytestmark = pytest.mark.django_db 12 | 13 | 14 | @pytest.mark.parametrize('model', [ 15 | pytest.lazy_fixture('event'), 16 | pytest.lazy_fixture('ticket'), 17 | pytest.lazy_fixture('cart'), 18 | pytest.lazy_fixture('cart_item'), 19 | pytest.lazy_fixture('purchase'), 20 | ]) 21 | def test_model_str_representation(model): 22 | assert str(model) 23 | 24 | 25 | @pytest.mark.parametrize('model', [ 26 | pytest.lazy_fixture('event'), 27 | pytest.lazy_fixture('ticket'), 28 | pytest.lazy_fixture('cart'), 29 | pytest.lazy_fixture('cart_item'), 30 | pytest.lazy_fixture('purchase'), 31 | ]) 32 | def test_model_id_field(model): 33 | assert isinstance(model.id, uuid.UUID) 34 | 35 | 36 | @pytest.mark.parametrize('quantity1,price1,quantity2,price2,expected_price', [ 37 | (1, Decimal('100.00'), 1, Decimal('100.00'), Decimal('200.00')), 38 | (2, Decimal('100.00'), 2, Decimal('100.00'), Decimal('400.00')), 39 | ]) 40 | def test_cart_price_property(quantity1, price1, quantity2, price2, expected_price, cart, ticket): 41 | CartItem.objects.create(cart=cart, ticket=ticket, quantity=quantity1, unit_price=price1) 42 | CartItem.objects.create(cart=cart, ticket=ticket, quantity=quantity2, unit_price=price2) 43 | assert cart.price == expected_price 44 | 45 | 46 | def test_cart_manager_get_cart_for_user(cart, user): 47 | cart = Cart.objects.get_cart_for_user(user) 48 | assert cart.user == user 49 | assert cart.closed is False 50 | assert Cart.objects.get_cart_for_user(user) == cart 51 | 52 | cart.closed = True 53 | cart.save() 54 | assert Cart.objects.get_cart_for_user(user) != cart 55 | 56 | 57 | def test_cart_manager_add_cart_item(cart, ticket): 58 | quantity = 1 59 | cart_item = Cart.objects.add_cart_item(cart, ticket, quantity) 60 | assert cart_item.cart == cart 61 | assert cart_item.ticket == ticket 62 | assert cart_item.quantity == quantity 63 | assert cart_item.unit_price == ticket.price 64 | 65 | cart_item = Cart.objects.add_cart_item(cart, ticket, quantity) 66 | assert cart_item.cart == cart 67 | assert cart_item.ticket == ticket 68 | assert cart_item.quantity == quantity + 1 69 | assert cart_item.unit_price == ticket.price 70 | 71 | 72 | @pytest.mark.parametrize('quantity,price,expected_price', [ 73 | (1, Decimal('100.00'), Decimal('100.00')), 74 | (2, Decimal('100.00'), Decimal('200.00')), 75 | ]) 76 | def test_cart_item_price_property(quantity, price, expected_price, cart, ticket): 77 | cart_item = CartItem.objects.create(cart=cart, ticket=ticket, quantity=quantity, unit_price=price) 78 | assert cart_item.price == expected_price 79 | 80 | 81 | def test_purchase_manager_create_purchase(cart): 82 | purchase = Purchase.objects.create_purchase(cart) 83 | assert purchase.user == cart.user 84 | assert purchase.cart == cart 85 | assert purchase.price == cart.price 86 | assert purchase.status == 'pending' 87 | assert purchase.pagseguro_redirect_url == '' 88 | 89 | 90 | @responses.activate 91 | def test_purchase_manager_create_checkout(cart_item, pagseguro_checkout_response): 92 | responses.add( 93 | responses.POST, 94 | 'https://ws.sandbox.pagseguro.uol.com.br/v2/checkout', 95 | body=pagseguro_checkout_response, 96 | status=200 97 | ) 98 | cart = cart_item.cart 99 | purchase = Purchase.objects.create_checkout(cart) 100 | assert purchase.pagseguro_redirect_url == 'https://sandbox.pagseguro.uol.com.br/v2/checkout/payment.html?code=36BCD3B352526D1EE4D6FFA359934D7E' 101 | assert cart.closed is True 102 | 103 | 104 | @responses.activate 105 | def test_purchase_manager_create_checkout_with_error(cart_item, pagseguro_checkout_error_response): 106 | responses.add( 107 | responses.POST, 108 | 'https://ws.sandbox.pagseguro.uol.com.br/v2/checkout', 109 | body=pagseguro_checkout_error_response, 110 | status=400 111 | ) 112 | cart = cart_item.cart 113 | with pytest.raises(CheckoutException): 114 | Purchase.objects.create_checkout(cart) 115 | 116 | 117 | @pytest.mark.parametrize('pagseguro_status,expected_status', [ 118 | ('2', 'pending'), 119 | ('3', 'paid'), 120 | ('4', 'pending'), 121 | ('5', 'pending'), 122 | ('6', 'pending'), 123 | ('7', 'canceled'), 124 | ('8', 'pending'), 125 | ('9', 'pending'), 126 | ]) 127 | def test_purchase_manager_update_purchase_status(pagseguro_status, expected_status, purchase, pagseguro_transaction): 128 | pagseguro_transaction['reference'] = str(purchase.id) 129 | pagseguro_transaction['status'] = pagseguro_status 130 | purchase = Purchase.objects.update_purchase_status(pagseguro_transaction) 131 | assert purchase.status == expected_status 132 | 133 | 134 | def test_purchase_manager_update_purchase_status_with_invalid_reference(pagseguro_transaction): 135 | pagseguro_transaction['reference'] = str(uuid.uuid4()) 136 | assert Purchase.objects.update_purchase_status(pagseguro_transaction) is None 137 | 138 | 139 | @responses.activate 140 | def test_update_purchase_status(client, purchase, pagseguro_notification, pagseguro_transaction_response): 141 | pagseguro_transaction_response = pagseguro_transaction_response.replace( 142 | '9fd05f66-315c-49f9-85f7-c92775f5a54d', 143 | '{}'.format(str(purchase.id)) 144 | ) 145 | pagseguro_transaction_response = pagseguro_transaction_response.replace( 146 | '1', '3' 147 | ) 148 | code = pagseguro_notification['notificationCode'] 149 | responses.add( 150 | responses.GET, 151 | 'https://ws.sandbox.pagseguro.uol.com.br/v2/transactions/notifications/{}'.format(code), 152 | body=pagseguro_transaction_response, 153 | status=200 154 | ) 155 | url = reverse('pagseguro_receive_notification') 156 | client.post(url, pagseguro_notification) 157 | purchase.refresh_from_db() 158 | assert purchase.status == 'paid' 159 | -------------------------------------------------------------------------------- /tests/tickets/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | import status 4 | from django.urls import reverse 5 | 6 | from apps.tickets.models import Purchase 7 | 8 | pytestmark = pytest.mark.django_db 9 | 10 | 11 | def test_event_list(admin_client, event): 12 | url = reverse('tickets:event_list') 13 | response = admin_client.get(url) 14 | assert response.status_code == status.HTTP_200_OK 15 | assert 'cart' in response.context 16 | assert event in response.context['events'] 17 | 18 | 19 | def test_event_detail(admin_client, event): 20 | url = reverse('tickets:event_detail', args=[event.id]) 21 | response = admin_client.get(url) 22 | assert response.status_code == status.HTTP_200_OK 23 | assert 'cart' in response.context 24 | assert response.context['event'] == event 25 | 26 | 27 | def test_cart_detail(admin_client): 28 | url = reverse('tickets:cart_detail') 29 | response = admin_client.get(url) 30 | assert response.status_code == status.HTTP_200_OK 31 | assert 'cart' in response.context 32 | 33 | 34 | def test_cart_clear(admin_client, admin_user, cart_item): 35 | url = reverse('tickets:cart_clear') 36 | cart = cart_item.cart 37 | assert cart.cart_items.count() == 1 38 | response = admin_client.post(url, follow=True) 39 | assert response.status_code == status.HTTP_200_OK 40 | assert cart.cart_items.count() == 0 41 | 42 | 43 | def test_cart_add_item(admin_client, cart, ticket): 44 | url = reverse('tickets:cart_add_item') 45 | assert cart.cart_items.count() == 0 46 | response = admin_client.post(url, {'ticket': ticket.id, 'quantity': 1}, follow=True) 47 | assert response.status_code == status.HTTP_200_OK 48 | assert cart.cart_items.count() == 1 49 | 50 | 51 | @responses.activate 52 | def test_purchase_create(admin_client, cart_item, pagseguro_checkout_response): 53 | responses.add( 54 | responses.POST, 55 | 'https://ws.sandbox.pagseguro.uol.com.br/v2/checkout', 56 | body=pagseguro_checkout_response, 57 | status=200 58 | ) 59 | url = reverse('tickets:purchase_create') 60 | response = admin_client.post(url, follow=True) 61 | assert response.status_code == status.HTTP_200_OK 62 | purchase = Purchase.objects.filter(user=cart_item.cart.user).first() 63 | assert purchase.status == 'pending' 64 | assert purchase.pagseguro_redirect_url 65 | 66 | 67 | def test_purchase_list(admin_client, purchase): 68 | url = reverse('tickets:purchase_list') 69 | response = admin_client.get(url) 70 | assert response.status_code == status.HTTP_200_OK 71 | assert purchase in response.context['purchases'] 72 | 73 | 74 | def test_purchase_detail(admin_client, purchase): 75 | url = reverse('tickets:purchase_detail', args=[purchase.id]) 76 | response = admin_client.get(url) 77 | assert response.status_code == status.HTTP_200_OK 78 | assert response.context['purchase'] == purchase 79 | --------------------------------------------------------------------------------