├── .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 |{{ event.description }}
10 |Item | 19 |Quantidade | 20 |Preço unitário | 21 |Preço | 22 |
---|---|---|---|
{{ cart_item.ticket.title }} | 28 |{{ cart_item.quantity }} | 29 |{{ cart_item.unit_price|intcomma }} | 30 |{{ cart_item.price|intcomma }} | 31 |
{{ event.description }}
12 |Preço: {{ ticket.price|intcomma }}
22 | 35 |Selecione o evento para verificar os tickets disponíveis.
11 |Id: {{ purchase.id }}
15 |Preço: {{ purchase.price|intcomma }}
16 |Status: {{ purchase.get_status_display }}
17 | {% if purchase.status == 'pending' %} 18 | 19 | {% endif %} 20 |Id | 20 |Preço | 21 |Status | 22 |Data | 23 |
---|---|---|---|
{{ purchase.id }} | 29 |{{ purchase.price|intcomma }} | 30 |{{ purchase.status }} | 31 |{{ purchase.created_at }} | 32 |
36BCD3B352526D1EE4D6FFA359934D7E
44 | 11029
55 | 5479CBCF-2E8F-4594-A5E5-4BF2038C98C1
75 | 101
82 |