├── .gitignore ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── djangovirtualpos ├── __init__.py ├── __version__.py ├── admin.py ├── debug.py ├── fields.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_vpospaymentoperation_response_code.py │ ├── 0003_auto_20170310_1829.py │ ├── 0004_auto_20170313_1401.py │ ├── 0005_auto_20170314_1057.py │ ├── 0006_vpospaymentoperation_environment.py │ ├── 0007_auto_20170320_1757.py │ ├── 0008_auto_20170321_1437.py │ ├── 0009_vpsrefundoperation.py │ ├── 0010_auto_20170329_1227.py │ ├── 0011_auto_20170404_1424.py │ ├── 0012_auto_20180209_1222.py │ ├── 0013_vposredsys_enable_preauth_policy.py │ ├── 0014_auto_20180403_1057.py │ └── __init__.py ├── models.py ├── static │ └── js │ │ └── djangovirtualpos │ │ └── payment │ │ └── set_payment_attributes.js ├── templates │ └── djangovirtualpos │ │ └── templatetags │ │ └── djangovirtualpos_js │ │ └── djangovirtualpos_js.html ├── templatetags │ ├── __init__.py │ └── djangovirtualpos_js.py ├── urls.py ├── util.py └── views.py ├── manual ├── COMMON.md └── vpos_types │ ├── BITPAY.md │ ├── CECA.md │ ├── PAYPAL.md │ └── REDSYS.md ├── setup.cfg └── setup.py /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # IDE 92 | .idea/ 93 | 94 | # MacOS thumbnails 95 | .DS_Store 96 | 97 | # Backup files 98 | *~ 99 | 100 | MANIFEST 101 | .pypirc 102 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # django-virtual-pos 2 | Django module that abstracts the flow of several virtual points of sale. 3 | 4 | # Releases 5 | ## 1.6.11 6 | - On refund operations, select the 'completed' VPOSPaymentOperation matching the sale_code if there are more than one 7 | 8 | ## 1.6.10 9 | - Managing refund async confirmations for Redsys 10 | 11 | ## 1.6.8 12 | - Integration with [BitPay](https://bitpay.com/) 13 | 14 | ## 1.6.7 15 | - Add DS_ERROR_CODE logging default message for unknown Ds_Response, allow SOAP responses with empty Ds_AuthorisationCode 16 | 17 | ## 1.6.6 18 | - Simplify integration. 19 | - Add example integration. 20 | 21 | ## 1.6.5 22 | - Add method to allow partial refund and full refund, specific to Redsys TPV Platform. 23 | - New model to register refund operations. 24 | - Add refund view example. 25 | 26 | 27 | ## 1.6.4 28 | - Include migrations. 29 | 30 | 31 | ## 1.6.4 32 | - Include migrations. 33 | 34 | 35 | ## 1.6.1 36 | - Allow reverse relation VPOSPaymentOperation -> VirtualPointOfSale 37 | 38 | 39 | ## 1.5 40 | - Adding environment to VPOSPaymentOperation 41 | - Changing labels in models 42 | 43 | 44 | ## 1.3 45 | - Fixing get_type_help_bug 46 | 47 | 48 | ## 1.2 49 | - Add new permission view_virtualpointofsale to ease management. 50 | - Add method specificit_vpos in VirtualPointOfSale that returns the specific model object according to the VPOS type. 51 | 52 | 53 | ## 1.1 54 | Minor changes in README.md. 55 | 56 | 57 | ## 1.0 Features 58 | - Integration with PayPal Checkout. 59 | - Integration with the following Spanish bank virtual POS: 60 | - [RedSyS](http://www.redsys.es/) 61 | - [Santander Elavon](https://www.santanderelavon.com/) 62 | - [CECA](http://www.cajasdeahorros.es/). 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 intelligenia soluciones informáticas 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude venv * 2 | recursive-exclude dist * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-virtual-pos 2 | Django module that abstracts the flow of several virtual points of sale including PayPal 3 | 4 | 5 | # What's this? 6 | 7 | This module abstracts the use of the most used virtual points of sale in Spain. 8 | 9 | # License 10 | 11 | [MIT LICENSE](LICENSE). 12 | 13 | # Implemented payment methods 14 | 15 | ### Paypal 16 | [Paypal](https://www.paypal.com/) paypal payment available. 17 | 18 | ### Bitpay 19 | [Bitpay](http://bitpay.com) bitcoin payments, from wallet to checkout 20 | 21 | ## Spanish Virtual Points of Sale 22 | 23 | 24 | ### Ceca 25 | [CECA](http://www.cajasdeahorros.es/) is the Spanish confederation of savings banks. 26 | 27 | ### RedSyS 28 | [RedSyS](http://www.redsys.es/) gives payment services to several Spanish banks like CaixaBank or Caja Rural. 29 | 30 | ### Santander Elavon 31 | [Santander Elavon](https://www.santanderelavon.com/) is one of the payment methods of the Spanish bank Santander. 32 | 33 | 34 | # Requirements and Installation 35 | 36 | ## Requirements 37 | 38 | - Python 2.7 (Python 3 not tested, contributors wanted!) 39 | - [Django](https://pypi.python.org/pypi/django) 40 | - [BeautifulSoup4](https://pypi.python.org/pypi/beautifulsoup4) 41 | - [lxml](https://pypi.python.org/pypi/lxml) 42 | - [pycrypto](https://pypi.python.org/pypi/pycrypto) 43 | - [Pytz](https://pypi.python.org/pypi/pytz) 44 | - [Requests](https://pypi.python.org/pypi/requests) 45 | 46 | 47 | Type: 48 | ````sh 49 | $ pip install django beautifulsoup4 lxml pycrypto pytz 50 | ```` 51 | 52 | ## Installation 53 | 54 | 55 | ### From PyPi 56 | 57 | ````sh 58 | $ pip install django-virtual-pos 59 | ```` 60 | 61 | ### From master branch 62 | 63 | Master branch will allways contain a working version of this module. 64 | 65 | ````sh 66 | $ pip install git+git://github.com/intelligenia/django-virtual-pos.git 67 | ```` 68 | 69 | 70 | ### settings.py 71 | 72 | Add the application djangovirtualpos to your settings.py: 73 | 74 | ````python 75 | INSTALLED_APPS = ( 76 | # ... 77 | "djangovirtualpos", 78 | ) 79 | ```` 80 | 81 | # Use 82 | 83 | See this [manual](manual/COMMON.md) (currently only in Spanish). 84 | 85 | ## Needed models 86 | 87 | You will need to implement this skeleton view using your own **Payment** model. 88 | 89 | This model has must have at least the following attributes: 90 | - **code**: sale code given by our system. 91 | - **operation_number**: bank operation number. 92 | - **status**: status of the payment: "paid", "pending" (**pending** is mandatory) or "canceled". 93 | - **amount**: amount to be charged. 94 | 95 | And the following methods: 96 | - **online_confirm**: mark the payment as paid. 97 | 98 | ## Integration examples 99 | - [djshop](https://github.com/diegojromerolopez/djshop) 100 | 101 | 102 | ## Needed views 103 | ### Sale summary view 104 | 105 | ````python 106 | def payment_summary(request, payment_id): 107 | """ 108 | Load a Payment object and show a summary of its contents to the user. 109 | """ 110 | 111 | payment = get_object_or_404(Payment, id=payment_id, status="pending") 112 | replacements = { 113 | "payment": payment, 114 | # ... 115 | } 116 | return render(request, '', replacements) 117 | 118 | ```` 119 | 120 | Note that this payment summary view should load a JS file called **set_payment_attributes.js**. 121 | 122 | This file is needed to set initial payment attributes according to which bank have the user selected. 123 | 124 | 125 | ### Payment_confirm view 126 | 127 | ````python 128 | @csrf_exempt 129 | def payment_confirmation(request, virtualpos_type): 130 | """ 131 | This view will be called by the bank. 132 | """ 133 | # Directly call to confirm_payment view 134 | 135 | # Or implement the following actions 136 | 137 | # Checking if the Point of Sale exists 138 | virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) 139 | 140 | if not virtual_pos: 141 | # The VPOS does not exist, inform the bank with a cancel 142 | # response if needed 143 | return VirtualPointOfSale.staticResponseNok(virtualpos_type) 144 | 145 | # Verify if bank confirmation is indeed from the bank 146 | verified = virtual_pos.verifyConfirmation() 147 | operation_number = virtual_pos.operation.operation_number 148 | 149 | with transaction.atomic(): 150 | try: 151 | # Getting your payment object from operation number 152 | payment = Payment.objects.get(operation_number=operation_number, status="pending") 153 | except Payment.DoesNotExist: 154 | return virtual_pos.responseNok("not_exists") 155 | 156 | if verified: 157 | # Charge the money and answer the bank confirmation 158 | try: 159 | response = virtual_pos.charge() 160 | # Implement the online_confirm method in your payment 161 | # this method will mark this payment as paid and will 162 | # store the payment date and time. 163 | payment.online_confirm() 164 | except VPOSCantCharge as e: 165 | return virtual_pos.responseNok(extended_status=e) 166 | except Exception as e: 167 | return virtual_pos.responseNok("cant_charge") 168 | 169 | else: 170 | # Payment could not be verified 171 | # signature is not right 172 | response = virtual_pos.responseNok("verification_error") 173 | 174 | return response 175 | ```` 176 | 177 | ### Payment ok view 178 | 179 | ````python 180 | def payment_ok(request, sale_code): 181 | """ 182 | Informs the user that the payment has been made successfully 183 | :param payment_code: Payment code. 184 | :param request: request. 185 | """ 186 | 187 | # Load your Payment model given its code 188 | payment = get_object_or_404(Payment, code=sale_code, status="paid") 189 | 190 | context = {'pay_status': "Done", "request": request} 191 | return render(request, '', {'context': context, 'payment': payment}) 192 | ```` 193 | 194 | ### Payment cancel view 195 | 196 | ````python 197 | def payment_cancel(request, sale_code): 198 | """ 199 | Informs the user that the payment has been canceled 200 | :param payment_code: Payment code. 201 | :param request: request. 202 | """ 203 | 204 | # Load your Payment model given its code 205 | payment = get_object_or_404(Payment, code=sale_code, status="pending") 206 | # Mark this payment as canceled 207 | payment.cancel() 208 | 209 | context = {'pay_status': "Done", "request": request} 210 | return render(request, '', {'context': context, 'payment': payment}) 211 | ```` 212 | 213 | ### Refund view 214 | 215 | ````python 216 | def refund(request, tpv, payment_code, amount, description): 217 | """ 218 | :param request: 219 | :param tpv: TPV Id 220 | :param payment_code: Payment code 221 | :param amount: Refund Amount (Example 10.89). 222 | :param description: Description of refund cause. 223 | :return: 224 | """ 225 | 226 | amount = Decimal(amount) 227 | 228 | try: 229 | # Checking if the Point of Sale exists 230 | tpv = VirtualPointOfSale.get(id=tpv) 231 | # Checking if the Payment exists 232 | payment = Payment.objects.get(code=payment_code, state="paid") 233 | 234 | except Payment.DoesNotExist as e: 235 | return http_bad_request_response_json_error(message=u"Does not exist payment with code {0}".format(payment_code)) 236 | 237 | refund_status = tpv.refund(payment_code, amount, description) 238 | 239 | if refund_status: 240 | message = u"Refund successful" 241 | else: 242 | message = u"Refund with erros" 243 | 244 | return http_response_json_ok(message) 245 | ```` 246 | 247 | 248 | # Authors 249 | - Mario Barchéin marioREMOVETHIS@REMOVETHISintelligenia.com 250 | - Diego J. Romero diegoREMOVETHIS@REMOVETHISintelligenia.com 251 | 252 | Remove REMOVETHIS to contact the authors. 253 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Virtual Pos 2 | ================== 3 | 4 | Django module that abstracts the flow of several virtual points of sale 5 | including PayPal 6 | 7 | What’s this? 8 | ------------------------- 9 | 10 | This module abstracts the use of the most used virtual points of sale in 11 | Spain. 12 | 13 | License 14 | ------------------------- 15 | 16 | `MIT LICENSE`_. 17 | 18 | Implemented payment methods 19 | ---------------------------- 20 | 21 | Paypal 22 | ------------------------- 23 | 24 | `Paypal`_ paypal payment available. 25 | 26 | Bitpay 27 | ------------------------- 28 | 29 | `Bitpay`_ bitcoin payments, from wallet to checkout 30 | 31 | Spanish Virtual Points of Sale 32 | ------------------------------ 33 | 34 | - Ceca 35 | 36 | 37 | `CECA`_ is the Spanish confederation of savings banks. 38 | 39 | - RedSyS 40 | 41 | `RedSyS`_ gives payment services to several Spanish banks like CaixaBank 42 | or Caja Rural. 43 | 44 | Santander Elavon 45 | ---------------- 46 | 47 | `Santander Elavon`_ is one of the payment methods of the Spanish bank 48 | Santander. 49 | 50 | Requirements and Installation 51 | ============================== 52 | 53 | Requirements 54 | ---------------- 55 | 56 | - Python 2.7 (Python 3 not tested, contributors wanted!) 57 | - ``Django`` 58 | - ``BeautifulSoup4`` 59 | - ``lxml`` 60 | - ``pycrypto`` 61 | - ``Pytz`` 62 | - ``Requests`` 63 | 64 | Type: 65 | 66 | .. code:: sh 67 | 68 | $ pip install django beautifulsoup4 lxml pycrypto pytz 69 | 70 | Installation 71 | ---------------- 72 | 73 | 74 | From PyPi 75 | ---------------- 76 | 77 | .. code:: sh 78 | 79 | $ pip install django-virtual-pos 80 | 81 | From master branch 82 | ------------------- 83 | 84 | Master branch will allways contain a working version of this module. 85 | 86 | .. code:: sh 87 | 88 | $ pip install git+git://github.com/intelligenia/django-virtual-pos.git 89 | 90 | settings.py 91 | ------------- 92 | 93 | Add the application djangovirtualpos to your settings.py: 94 | 95 | .. code:: python 96 | 97 | INSTALLED_APPS = ( 98 | # ... 99 | "djangovirtualpos", 100 | ) 101 | 102 | Use 103 | ---- 104 | 105 | See this ``manual`` (currently only in Spanish). 106 | 107 | Needed models 108 | ------------- 109 | 110 | You will need to implement this skeleton view using your own **Payment** 111 | model. 112 | 113 | This model has must have at least the following attributes: - **code**: 114 | sale code given by our system. - **operation_number**: bank operation 115 | number. - **status**: status of the payment: “paid”, “pending” 116 | (**pending** is mandatory) or “canceled”. - **amount**: amount to be 117 | charged. 118 | 119 | And the following methods: - **online_confirm**: mark the payment as 120 | paid. 121 | 122 | Integration examples 123 | ----------------------- 124 | 125 | - ``djshop`` 126 | 127 | Needed views 128 | -------------- 129 | 130 | Sale summary view 131 | ------------------ 132 | 133 | .. code:: python 134 | 135 | def payment_summary(request, payment_id): 136 | """ 137 | Load a Payment object and show a summary of its contents to the user. 138 | """ 139 | 140 | payment = get_object_or_404(Payment, id=payment_id, status="pending") 141 | replacements = { 142 | "payment": payment, 143 | # ... 144 | } 145 | return render(request, '', replacements) 146 | 147 | Note that this payment summary view should load a JS file called 148 | **set_payment_attributes.js**. 149 | 150 | This file is needed to set initial payment attributes according to which 151 | bank have the user selected. 152 | 153 | Payment_confirm view 154 | ------------------------- 155 | 156 | .. code:: python 157 | 158 | @csrf_exempt 159 | def payment_confirmation(request, virtualpos_type): 160 | """ 161 | This view will be called by the bank. 162 | """ 163 | # Directly call to confirm_payment view 164 | 165 | # Or implement the following actions 166 | 167 | # Checking if the Point of Sale exists 168 | virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) 169 | 170 | if not virtual_pos: 171 | # The VPOS does not exist, inform the bank with a cancel 172 | # response if needed 173 | return VirtualPointOfSale.staticResponseNok(virtualpos_type) 174 | 175 | # Verify if bank confirmation is indeed from the bank 176 | verified = virtual_pos.verifyConfirmation() 177 | operation_number = virtual_pos.operation.operation_number 178 | 179 | with transaction.atomic(): 180 | try: 181 | # Getting your payment object from operation number 182 | payment = Payment.objects.get(operation_number=operation_number, status="pending") 183 | except Payment.DoesNotExist: 184 | return virtual_pos.responseNok("not_exists") 185 | 186 | if verified: 187 | # Charge the money and answer the bank confirmation 188 | try: 189 | response = virtual_pos.charge() 190 | # Implement the online_confirm method in your payment 191 | # this method will mark this payment as paid and will 192 | # store the payment date and time. 193 | payment.online_confirm() 194 | except VPOSCantCharge as e: 195 | return virtual_pos.responseNok(extended_status=e) 196 | except Exception as e: 197 | return virtual_pos.responseNok("cant_charge") 198 | 199 | else: 200 | # Payment could not be verified 201 | # signature is not right 202 | response = virtual_pos.responseNok("verification_error") 203 | 204 | return response 205 | 206 | Payment ok view 207 | ------------------------- 208 | 209 | .. code:: python 210 | 211 | def payment_ok(request, sale_code): 212 | """ 213 | Informs the user that the payment has been made successfully 214 | :param payment_code: Payment code. 215 | :param request: request. 216 | """ 217 | 218 | # Load your Payment model given its code 219 | payment = get_object_or_404(Payment, code=sale_code, status="paid") 220 | 221 | context = {'pay_status': "Done", "request": request} 222 | return render(request, '', {'context': context, 'payment': payment}) 223 | 224 | Payment cancel view 225 | -------------------- 226 | 227 | .. code:: python 228 | 229 | def payment_cancel(request, sale_code): 230 | """ 231 | Informs the user that the payment has been canceled 232 | :param payment_code: Payment code. 233 | :param request: request. 234 | """ 235 | 236 | # Load your Payment model given its code 237 | payment = get_object_or_404(Payment, code=sale_code, status="pending") 238 | # Mark this payment as canceled 239 | payment.cancel() 240 | 241 | context = {'pay_status': "Done", "request": request} 242 | return render(request, '', {'context': context, 'payment': payment}) 243 | 244 | 245 | Refund view 246 | ----------- 247 | 248 | .. code:: python 249 | 250 | def refund(request, tpv, payment_code, amount, description): 251 | """ 252 | :param request: 253 | :param tpv: TPV Id 254 | :param payment_code: Payment code 255 | :param amount: Refund Amount (Example 10.89). 256 | :param description: Description of refund cause. 257 | :return: 258 | """ 259 | 260 | amount = Decimal(amount) 261 | 262 | try: 263 | # Checking if the Point of Sale exists 264 | tpv = VirtualPointOfSale.get(id=tpv) 265 | # Checking if the Payment exists 266 | payment = Payment.objects.get(code=payment_code, state="paid") 267 | 268 | except Payment.DoesNotExist as e: 269 | return http_bad_request_response_json_error(message=u"Does not exist payment with code {0}".format(payment_code)) 270 | 271 | refund_status = tpv.refund(payment_code, amount, description) 272 | 273 | if refund_status: 274 | message = u"Refund successful" 275 | else: 276 | message = u"Refund with erros" 277 | 278 | return http_response_json_ok(message) 279 | 280 | Authors 281 | =============== 282 | 283 | - Mario Barchéin marioREMOVETHIS@REMOVETHISintelligenia.com 284 | - Diego J. Romero diegoREMOVETHIS@REMOVETHISintelligenia.com 285 | 286 | Remove REMOVETHIS to contact the authors. 287 | 288 | 289 | .. _MIT LICENSE: LICENSE 290 | .. _Paypal: https://www.paypal.com/ 291 | .. _Bitpay: http://bitpay.com 292 | .. _CECA: http://www.cajasdeahorros.es/ 293 | .. _RedSyS: http://www.redsys.es/ 294 | .. _Santander Elavon: https://www.santanderelavon.com/ 295 | .. _Django: https://pypi.python.org/pypi/django 296 | .. _BeautifulSoup4: https://pypi.python.org/pypi/beautifulsoup4 297 | .. _lxml: https://pypi.python.org/pypi/lxml 298 | .. _pycrypto: https://pypi.python.org/pypi/pycrypto 299 | .. _Pytz: https://pypi.python.org/pypi/pytz 300 | .. _Requests: https://pypi.python.org/pypi/requests 301 | .. _manual: manual/COMMON.md 302 | .. _djshop: https://github.com/diegojromerolopez/djshop -------------------------------------------------------------------------------- /djangovirtualpos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelligenia/django-virtual-pos/583abd4ed988738bcf6bd6a024478048160fe3d4/djangovirtualpos/__init__.py -------------------------------------------------------------------------------- /djangovirtualpos/__version__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 7, 1) 2 | 3 | __version__ = '.'.join(map(str, VERSION)) 4 | -------------------------------------------------------------------------------- /djangovirtualpos/admin.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from django.contrib import admin 4 | from djangovirtualpos.models import VirtualPointOfSale, VPOSRefundOperation, VPOSCeca, VPOSRedsys, VPOSSantanderElavon, VPOSPaypal, VPOSBitpay 5 | 6 | admin.site.register(VirtualPointOfSale) 7 | admin.site.register(VPOSRefundOperation) 8 | admin.site.register(VPOSCeca) 9 | admin.site.register(VPOSRedsys) 10 | admin.site.register(VPOSPaypal) 11 | admin.site.register(VPOSSantanderElavon) 12 | admin.site.register(VPOSBitpay) 13 | 14 | 15 | -------------------------------------------------------------------------------- /djangovirtualpos/debug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals, print_function 4 | 5 | import inspect 6 | from django.conf import settings 7 | from django.utils import timezone 8 | 9 | import logging 10 | 11 | 12 | # Prepare debug message 13 | def _prepare_debuglog_message(message, caller_level=2): 14 | # Por si acaso se le mete una cadena de tipo str, 15 | # este módulo es capaz de detectar eso y convertirla a UTF8 16 | if type(message) == str: 17 | message = unicode(message, "UTF-8") 18 | 19 | # Hora 20 | now = timezone.now() 21 | 22 | # Contexto desde el que se ha llamado 23 | curframe = inspect.currentframe() 24 | 25 | # Objeto frame que llamó a dlprint 26 | calframes = inspect.getouterframes(curframe, caller_level) 27 | caller_frame = calframes[2][0] 28 | caller_name = calframes[2][3] 29 | 30 | # Ruta del archivo que llamó a dlprint 31 | filename_path = caller_frame.f_code.co_filename 32 | filename = filename_path.split("/")[-1] 33 | 34 | # Obtención del mensaje 35 | return u"DjangoVirtualPOS: {0} {1} \"{2}\" at {3}:{5} in {6} ({4}:{5})\n".format( 36 | now.strftime("%Y-%m-%d %H:%M:%S %Z"), settings.DOMAIN, message, 37 | filename, filename_path, caller_frame.f_lineno, caller_name 38 | ) 39 | 40 | 41 | # Prints the debug message 42 | def dlprint(message): 43 | logger = logging.getLogger("syslog") 44 | complete_message = _prepare_debuglog_message(message=message, caller_level=3) 45 | utf8_complete_message = complete_message.encode('UTF-8') 46 | logger.debug(utf8_complete_message) 47 | print(utf8_complete_message) 48 | -------------------------------------------------------------------------------- /djangovirtualpos/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.forms import forms 4 | from django.forms.models import ModelMultipleChoiceField 5 | from django.contrib.admin.widgets import FilteredSelectMultiple 6 | from django.db.models import Count 7 | 8 | from djangovirtualpos import models 9 | 10 | 11 | class VPOSField(ModelMultipleChoiceField): 12 | """ Campo personalizado para la selección de TPVs """ 13 | 14 | def __init__(self, queryset=None, required=True, 15 | widget=None, label="VPOSs", initial=None, 16 | help_text="TPVs que se podrán utilizar para realizar el pago online.", *args, **kwargs): 17 | 18 | if queryset is None: 19 | queryset = models.VPOS.objects.filter(is_deleted=False) 20 | 21 | if widget is None: 22 | widget = FilteredSelectMultiple("", False) 23 | 24 | super(VPOSField, self).__init__( 25 | queryset=queryset, 26 | required=required, 27 | widget=widget, 28 | label=label, 29 | initial=initial, 30 | help_text=help_text, 31 | *args, **kwargs) 32 | 33 | def clean(self, value): 34 | if value: 35 | # Si se reciben valores (id's de Tpvs), cargarlos para comprobar si son todos del mismo tipo. 36 | # Construimos un ValuesQuerySet con sólo el campo "type", hacemos la cuenta y ordenamos en orden descendente para comprobar el primero (esto es como hacer un "group_by" y un count) 37 | # Si el primero es mayor que 1 mostramos el error oportuno. 38 | count = models.VPOS.objects.filter(id__in=value).values("type").annotate(Count("type")).order_by( 39 | "-type__count") 40 | if count[0]["type__count"] > 1: 41 | raise forms.ValidationError("Asegúrese de no seleccionar más de un TPV del tipo '{0}'".format( 42 | dict(models.VPOS_TYPES)[count[0]["type"]])) 43 | return super(VPOSField, self).clean(value) 44 | -------------------------------------------------------------------------------- /djangovirtualpos/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django import forms 4 | from models import VPOSCeca, VPOSRedsys, VPOSPaypal, VPOSSantanderElavon 5 | 6 | from django.conf import settings 7 | from models import VPOS_TYPES 8 | 9 | 10 | class TrimForm(forms.ModelForm): 11 | """Django no hace trim por defecto, así que tenemos que crear esta clase 12 | para hacerlo nosotros de forma explícita a la hora de realizar la 13 | validación del formulario.""" 14 | 15 | def clean(self): 16 | cleaned_data = super(TrimForm, self).clean() 17 | 18 | for field in self.cleaned_data: 19 | if isinstance(self.cleaned_data[field], basestring): 20 | self.cleaned_data[field] = self.cleaned_data[field].strip() 21 | 22 | # Hay que devolver siempre el array "cleaned_data" 23 | return cleaned_data 24 | 25 | 26 | # Habrá que añadir un formulario nuevo como este para cada uno de los distintos 27 | # TPVs que se añadan 28 | class VPOSCecaForm(TrimForm): 29 | """Formulario para el modelo TpvCeca.""" 30 | 31 | class Meta: 32 | model = VPOSCeca 33 | exclude = ("type", "is_erased") 34 | 35 | 36 | class VPOSRedsysForm(TrimForm): 37 | """Formulario para el modelo TpvRedsys.""" 38 | 39 | class Meta: 40 | model = VPOSRedsys 41 | exclude = ("type", "is_erased") 42 | 43 | 44 | class VPOSPaypalForm(TrimForm): 45 | """Formulario para el modelo TpvPaypal.""" 46 | 47 | class Meta: 48 | model = VPOSPaypal 49 | exclude = ("type", "is_erased") 50 | 51 | 52 | class VPOSSantanderElavonForm(TrimForm): 53 | """Formulario para el modelo TpvSantanderElavon.""" 54 | 55 | class Meta: 56 | model = VPOSSantanderElavon 57 | exclude = ("type", "is_erased") 58 | 59 | 60 | # Esta variable habrá que actualizarla cada vez que se añada un formulario 61 | # para un TPV 62 | VPOS_FORMS = { 63 | "ceca": VPOSCecaForm, 64 | "redsys": VPOSRedsysForm, 65 | "paypal": VPOSPaypalForm, 66 | "santanderelavon": VPOSSantanderElavonForm 67 | } 68 | 69 | 70 | class VPOSTypeForm(forms.Form): 71 | """Formulario para la selección de tipo de TPV a crear en un paso 72 | posterior. 73 | 74 | Esta formado por un campo "select" en el que los valores son aquellos 75 | contenidos en la variable settings.TPVS o todos los disponibles en 76 | TPV_TYPES en models.py 77 | """ 78 | 79 | def __init__(self, *args, **kwargs): 80 | super(VPOSTypeForm, self).__init__(*args, **kwargs) 81 | 82 | if hasattr(settings, "ENABLED_VPOS_LIST") and settings.ENABLED_VPOS_LIST: 83 | vpos_types = settings.ENABLED_VPOS_LIST 84 | else: 85 | vpos_types = VPOS_TYPES 86 | 87 | self.fields["type"] = forms.ChoiceField(choices=vpos_types, required=True, label="Tipo de TPV", 88 | help_text="Tipo de pasarela de pago a crear.") 89 | 90 | 91 | class DeleteForm(forms.Form): 92 | """Formulario para confirmación de borrado de elementos""" 93 | erase = forms.BooleanField(required=False, label="Borrar") 94 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.2 on 2016-12-12 16:30 3 | from __future__ import unicode_literals 4 | 5 | import django.core.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import re 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='VirtualPointOfSale', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('name', models.CharField(max_length=128, verbose_name='Nombre')), 24 | ('bank_name', models.CharField(max_length=128, verbose_name='Nombre de la entidad bancaria')), 25 | ('type', models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon')], default='', max_length=16, verbose_name='Tipo de TPV')), 26 | ('distributor_name', models.CharField(blank=True, help_text='Raz\xf3n social del distribuidor de entradas.', max_length=512, verbose_name='Raz\xf3n social del distribuidor de entradas')), 27 | ('distributor_cif', models.CharField(blank=True, help_text='C.I.F. del distribuidor de entradas.', max_length=150, verbose_name='CIF del distribuidor de entradas')), 28 | ('environment', models.CharField(choices=[('testing', 'Pruebas'), ('production', 'Producci\xf3n')], default='testing', help_text="Entorno de ejecuci\xf3n en el que se encuentra el TPV. Una vez que el TPV est\xe9 en entorno de 'producci\xf3n' no cambie a entorno de 'pruebas' a no ser que est\xe9 seguro de lo que hace.", max_length=16, verbose_name='Entorno de ejecuci\xf3n del TPV')), 29 | ('has_partial_refunds', models.BooleanField(default=False, help_text='Indica si se pueden realizar devoluciones por un importe menor que el total de la venta (por ejemplo, para devolver tickets individuales).', verbose_name='Indica si tiene devoluciones parciales.')), 30 | ('has_total_refunds', models.BooleanField(default=False, help_text='Indica si se pueden realizar devoluciones por un importe igual al total de la venta.', verbose_name='Indica si tiene devoluciones totales.')), 31 | ('is_erased', models.BooleanField(default=False, help_text='Indica si el TPV est\xe1 eliminado de forma l\xf3gica.', verbose_name='Indica si el TPV est\xe1 eliminado.')), 32 | ], 33 | options={ 34 | 'ordering': ['name'], 35 | 'verbose_name': 'virtual point of sale', 36 | 'verbose_name_plural': 'virtual points of sale', 37 | }, 38 | ), 39 | migrations.CreateModel( 40 | name='VPOSPaymentOperation', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('amount', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Coste de la operaci\xf3n')), 44 | ('description', models.CharField(max_length=512, verbose_name='Descripci\xf3n de la venta')), 45 | ('url_ok', models.CharField(help_text='URL a la que redirige la pasarela bancaria cuando la compra ha sido un \xe9xito', max_length=255, verbose_name='URL de OK')), 46 | ('url_nok', models.CharField(help_text='URL a la que redirige la pasarela bancaria cuando la compra ha fallado', max_length=255, verbose_name='URL de NOK')), 47 | ('operation_number', models.CharField(max_length=255, verbose_name='N\xfamero de operaci\xf3n')), 48 | ('confirmation_code', models.CharField(max_length=255, null=True, verbose_name='C\xf3digo de confirmaci\xf3n enviado por el banco.')), 49 | ('confirmation_data', models.TextField(null=True, verbose_name='POST enviado por la pasarela bancaria al confirmar la compra.')), 50 | ('sale_code', models.CharField(help_text='C\xf3digo de la venta seg\xfan la aplicaci\xf3n.', max_length=512, verbose_name='C\xf3digo de la venta')), 51 | ('status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed')], max_length=64, verbose_name='Estado del pago')), 52 | ('creation_datetime', models.DateTimeField(verbose_name='Fecha de creaci\xf3n del objeto')), 53 | ('last_update_datetime', models.DateTimeField(verbose_name='Fecha de \xfaltima actualizaci\xf3n del objeto')), 54 | ('type', models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon')], default='', max_length=16, verbose_name='Tipo de TPV')), 55 | ('virtual_point_of_sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, parent_link=True, related_name='+', to='djangovirtualpos.VirtualPointOfSale')), 56 | ], 57 | ), 58 | migrations.CreateModel( 59 | name='VPOSCeca', 60 | fields=[ 61 | ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), 62 | ('merchant_id', models.CharField(max_length=9, validators=[django.core.validators.MinLengthValidator(9), django.core.validators.MaxLengthValidator(9), django.core.validators.RegexValidator(message='Aseg\xfarese de que todos los caracteres son n\xfameros', regex=re.compile('^\\d*$'))], verbose_name='MerchantID')), 63 | ('acquirer_bin', models.CharField(max_length=10, validators=[django.core.validators.MinLengthValidator(10), django.core.validators.MaxLengthValidator(10), django.core.validators.RegexValidator(message='Aseg\xfarese de que todos los caracteres son n\xfameros', regex=re.compile('^\\d*$'))], verbose_name='AcquirerBIN')), 64 | ('terminal_id', models.CharField(max_length=8, validators=[django.core.validators.MinLengthValidator(8), django.core.validators.MaxLengthValidator(8), django.core.validators.RegexValidator(message='Aseg\xfarese de que todos los caracteres son n\xfameros', regex=re.compile('^\\d*$'))], verbose_name='TerminalID')), 65 | ('encryption_key_testing', models.CharField(max_length=10, validators=[django.core.validators.MinLengthValidator(8), django.core.validators.MaxLengthValidator(10)], verbose_name='Encryption Key para el entorno de pruebas')), 66 | ('encryption_key_production', models.CharField(blank=True, max_length=10, validators=[django.core.validators.MinLengthValidator(8), django.core.validators.MaxLengthValidator(10)], verbose_name='Encryption Key para el entorno de producci\xf3n')), 67 | ('operation_number_prefix', models.CharField(blank=True, max_length=20, validators=[django.core.validators.MinLengthValidator(0), django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator(message='Aseg\xfarese de s\xf3lo use caracteres alfanum\xe9ricos', regex=re.compile('^[A-Za-z0-9]*$'))], verbose_name='Prefijo del n\xfamero de operaci\xf3n')), 68 | ], 69 | bases=('djangovirtualpos.virtualpointofsale',), 70 | ), 71 | migrations.CreateModel( 72 | name='VPOSPaypal', 73 | fields=[ 74 | ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), 75 | ('API_username', models.CharField(max_length=60, verbose_name='API_username')), 76 | ('API_password', models.CharField(max_length=60, verbose_name='API_password')), 77 | ('API_signature', models.CharField(max_length=60, verbose_name='API_signature')), 78 | ('Version', models.CharField(max_length=3, verbose_name='Version')), 79 | ], 80 | bases=('djangovirtualpos.virtualpointofsale',), 81 | ), 82 | migrations.CreateModel( 83 | name='VPOSRedsys', 84 | fields=[ 85 | ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), 86 | ('merchant_code', models.CharField(max_length=9, verbose_name='MerchantCode')), 87 | ('merchant_response_url', models.URLField(help_text='Confirmation URL that will be used by the virtual POS', max_length=64, verbose_name='MerchantURL')), 88 | ('terminal_id', models.CharField(max_length=3, verbose_name='TerminalID')), 89 | ('encryption_key_testing', models.CharField(default=None, max_length=20, null=True, verbose_name='Encryption Key para el entorno de pruebas (OBSOLETO)')), 90 | ('encryption_key_production', models.CharField(default=None, max_length=20, null=True, verbose_name='Encryption Key para el entorno de producci\xf3n (OBSOLETO)')), 91 | ('encryption_key_testing_sha256', models.CharField(default=None, max_length=64, null=True, verbose_name='Encryption Key SHA-256 para el entorno de pruebas')), 92 | ('encryption_key_production_sha256', models.CharField(default=None, max_length=64, null=True, verbose_name='Encryption Key SHA-256 para el entorno de producci\xf3n')), 93 | ('operation_number_prefix', models.CharField(blank=True, max_length=3, validators=[django.core.validators.MinLengthValidator(0), django.core.validators.MaxLengthValidator(3), django.core.validators.RegexValidator(message='Aseg\xfarese de s\xf3lo use caracteres num\xe9ricos', regex=re.compile('^\\d+$'))], verbose_name='Prefijo del n\xfamero de operaci\xf3n')), 94 | ], 95 | bases=('djangovirtualpos.virtualpointofsale',), 96 | ), 97 | migrations.CreateModel( 98 | name='VPOSSantanderElavon', 99 | fields=[ 100 | ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), 101 | ('merchant_id', models.CharField(max_length=50, validators=[django.core.validators.MinLengthValidator(1), django.core.validators.MaxLengthValidator(50), django.core.validators.RegexValidator(message='Aseg\xfarese de que todos los caracteres son alfanum\xe9ricos', regex=re.compile('^[a-zA-Z0-9]*$'))], verbose_name='MerchantID')), 102 | ('merchant_response_url', models.URLField(help_text='Confirmation URL that will be used by the virtual POS', max_length=64, verbose_name='MerchantURL')), 103 | ('account', models.CharField(max_length=30, validators=[django.core.validators.MinLengthValidator(0), django.core.validators.MaxLengthValidator(30), django.core.validators.RegexValidator(message='Aseg\xfarese de que todos los caracteres son alfanum\xe9ricos', regex=re.compile('^[a-zA-Z0-9.]*$'))], verbose_name='Account')), 104 | ('encryption_key', models.CharField(max_length=64, validators=[django.core.validators.MinLengthValidator(8), django.core.validators.MaxLengthValidator(10)], verbose_name='Clave secreta de cifrado')), 105 | ('operation_number_prefix', models.CharField(blank=True, max_length=20, validators=[django.core.validators.MinLengthValidator(0), django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator(message='Aseg\xfarese de s\xf3lo use caracteres alfanum\xe9ricos', regex=re.compile('^[A-Za-z0-9]*$'))], verbose_name='Prefijo del n\xfamero de operaci\xf3n')), 106 | ], 107 | bases=('djangovirtualpos.virtualpointofsale',), 108 | ), 109 | ] 110 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0002_vpospaymentoperation_response_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='vpospaymentoperation', 16 | name='response_code', 17 | field=models.CharField(max_length=255, null=True, verbose_name='C\xf3digo de respuesta con estado de aceptaci\xf3n o denegaci\xf3n de la operaci\xf3n.'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0003_auto_20170310_1829.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0002_vpospaymentoperation_response_code'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='vposredsys', 16 | name='encryption_key_production', 17 | ), 18 | migrations.RemoveField( 19 | model_name='vposredsys', 20 | name='encryption_key_testing', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0004_auto_20170313_1401.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0003_auto_20170310_1829'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='virtualpointofsale', 16 | options={'ordering': ['name'], 'verbose_name': 'virtual point of sale', 'verbose_name_plural': 'virtual points of sale', 'permissions': (('view_virtualpointofsale', 'Can view Virtual Points of Sale'),)}, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0005_auto_20170314_1057.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0004_auto_20170313_1401'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='virtualpointofsale', 16 | options={'ordering': ['name'], 'verbose_name': 'virtual point of sale', 'verbose_name_plural': 'virtual points of sale', 'permissions': (('view_virtualpointofsale', 'View Virtual Points of Sale'),)}, 17 | ), 18 | migrations.AlterField( 19 | model_name='virtualpointofsale', 20 | name='distributor_cif', 21 | field=models.CharField(help_text='C.I.F. del distribuidor.', max_length=150, verbose_name='CIF del distribuidor', blank=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='virtualpointofsale', 25 | name='distributor_name', 26 | field=models.CharField(help_text='Raz\xf3n social del distribuidor.', max_length=512, verbose_name='Raz\xf3n social del distribuidor', blank=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0006_vpospaymentoperation_environment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0005_auto_20170314_1057'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='vpospaymentoperation', 16 | name='environment', 17 | field=models.CharField(default='', max_length=255, verbose_name='Entorno del TPV', blank=True, choices=[('testing', 'Pruebas'), ('production', 'Producci\xf3n')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0007_auto_20170320_1757.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0006_vpospaymentoperation_environment'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='vpospaymentoperation', 16 | name='sale_code', 17 | field=models.CharField(help_text='C\xf3digo de la venta seg\xfan la aplicaci\xf3n.', unique=True, max_length=255, verbose_name='C\xf3digo de la venta'), 18 | ), 19 | migrations.AlterField( 20 | model_name='vpospaymentoperation', 21 | name='virtual_point_of_sale', 22 | field=models.ForeignKey(parent_link=True, related_name='payment_operations', to='djangovirtualpos.VirtualPointOfSale'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0008_auto_20170321_1437.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangovirtualpos', '0007_auto_20170320_1757'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='vpospaymentoperation', 16 | name='sale_code', 17 | field=models.CharField(help_text='C\xf3digo de la venta seg\xfan la aplicaci\xf3n.', max_length=512, verbose_name='C\xf3digo de la venta'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0009_vpsrefundoperation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-03-28 16:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('djangovirtualpos', '0008_auto_20170321_1437'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='VPOSRefundOperation', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('amount', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Cantidad de la devoluci\xf3n')), 21 | ('description', models.CharField(max_length=512, verbose_name='Descripci\xf3n de la devoluci\xf3n')), 22 | ('operation_number', models.CharField(max_length=255, verbose_name='N\xfamero de operaci\xf3n')), 23 | ('confirmation_code', models.CharField(max_length=255, null=True, verbose_name='C\xf3digo de confirmaci\xf3n enviado por el banco.')), 24 | ('status', models.CharField(choices=[('completed', 'Completed'), ('failed', 'Failed')], max_length=64, verbose_name='Estado de la devoluci\xf3n')), 25 | ('creation_datetime', models.DateTimeField(verbose_name='Fecha de creaci\xf3n del objeto')), 26 | ('last_update_datetime', models.DateTimeField(verbose_name='Fecha de \xfaltima actualizaci\xf3n del objeto')), 27 | ], 28 | ), 29 | migrations.AlterField( 30 | model_name='vpospaymentoperation', 31 | name='status', 32 | field=models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('partially_refunded', 'Partially Refunded'), ('completely_refunded', 'Completely Refunded')], max_length=64, verbose_name='Estado del pago'), 33 | ), 34 | migrations.AddField( 35 | model_name='vposrefundoperation', 36 | name='payment', 37 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_operation', to='djangovirtualpos.VPOSPaymentOperation'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0010_auto_20170329_1227.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-03-29 12:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('djangovirtualpos', '0009_vpsrefundoperation'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='vposrefundoperation', 17 | name='confirmation_code', 18 | ), 19 | migrations.AlterField( 20 | model_name='vposrefundoperation', 21 | name='status', 22 | field=models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed')], max_length=64, verbose_name='Estado de la devoluci\xf3n'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0011_auto_20170404_1424.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-04 14:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('djangovirtualpos', '0010_auto_20170329_1227'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='vposrefundoperation', 18 | name='payment', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='refund_operations', to='djangovirtualpos.VPOSPaymentOperation'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0012_auto_20180209_1222.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2018-02-09 12:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('djangovirtualpos', '0011_auto_20170404_1424'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='VPOSBitpay', 18 | fields=[ 19 | ('parent', models.OneToOneField(db_column='vpos_id', on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='+', serialize=False, to='djangovirtualpos.VirtualPointOfSale')), 20 | ('testing_api_key', models.CharField(blank=True, max_length=512, null=True, verbose_name='API Key de Bitpay para entorno de test')), 21 | ('production_api_key', models.CharField(max_length=512, verbose_name='API Key de Bitpay para entorno de producci\xf3n')), 22 | ('currency', models.CharField(choices=[('EUR', 'Euro'), ('USD', 'Dolares'), ('BTC', 'Bitcoin')], default='EUR', max_length=3, verbose_name='Moneda (EUR, USD, BTC)')), 23 | ('transaction_speed', models.CharField(choices=[('high', 'Alta'), ('medium', 'Media'), ('low', 'Baja')], default='medium', max_length=10, verbose_name='Velocidad de la operaci\xf3n')), 24 | ('notification_url', models.URLField(verbose_name='Url notificaciones actualizaci\xf3n estados (https)')), 25 | ('operation_number_prefix', models.CharField(blank=True, max_length=20, null=True, verbose_name='Prefijo del n\xfamero de operaci\xf3n')), 26 | ], 27 | bases=('djangovirtualpos.virtualpointofsale',), 28 | ), 29 | migrations.AlterField( 30 | model_name='virtualpointofsale', 31 | name='type', 32 | field=models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon'), ('bitpay', 'TPV Bitpay')], default='', max_length=16, verbose_name='Tipo de TPV'), 33 | ), 34 | migrations.AlterField( 35 | model_name='vpospaymentoperation', 36 | name='type', 37 | field=models.CharField(choices=[('ceca', 'TPV Virtual - Confederaci\xf3n Espa\xf1ola de Cajas de Ahorros (CECA)'), ('paypal', 'Paypal'), ('redsys', 'TPV Redsys'), ('santanderelavon', 'TPV Santander Elavon'), ('bitpay', 'TPV Bitpay')], default='', max_length=16, verbose_name='Tipo de TPV'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0013_vposredsys_enable_preauth_policy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2018-04-03 09:53 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('djangovirtualpos', '0012_auto_20180209_1222'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='vposredsys', 17 | name='enable_preauth_policy', 18 | field=models.BooleanField(default=False, verbose_name='Habilitar pol\xedtica de preautorizaci\xf3n'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/0014_auto_20180403_1057.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2018-04-03 10:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('djangovirtualpos', '0013_vposredsys_enable_preauth_policy'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='vposredsys', 17 | name='enable_preauth_policy', 18 | ), 19 | migrations.AddField( 20 | model_name='vposredsys', 21 | name='operative_type', 22 | field=models.CharField(choices=[('authorization', 'autorizaci\xf3n'), ('pre-authorization', 'pre-autorizaci\xf3n')], default='authorization', max_length=512, verbose_name='Tipo de operativa'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /djangovirtualpos/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelligenia/django-virtual-pos/583abd4ed988738bcf6bd6a024478048160fe3d4/djangovirtualpos/migrations/__init__.py -------------------------------------------------------------------------------- /djangovirtualpos/static/js/djangovirtualpos/payment/set_payment_attributes.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | /** Show error message */ 4 | function show_error(message){ 5 | 6 | console.error(message); 7 | 8 | if(PNotify){ 9 | $(function(){ 10 | new PNotify({ 11 | title: 'Error', 12 | text: message, 13 | type: 'error', 14 | desktop: { 15 | desktop: true 16 | } 17 | }); 18 | }); 19 | } 20 | } 21 | 22 | /* Payment */ 23 | $(".pay_button").click(function(event){ 24 | 25 | $(".pay_button").attr("disabled", true); 26 | $(".pay_button").addClass("disabled"); 27 | 28 | 29 | var $this = $(this); 30 | var url = vpos_constants["SET_PAYMENT_ATTRIBUTES_URL"]; 31 | var $form = $(this).parents("form"); 32 | var post = { 33 | payment_code: vpos_constants["PAYMENT_CODE"], 34 | url_ok: vpos_constants["URL_OK"], 35 | url_nok: vpos_constants["URL_NOK"], 36 | vpos_id: $(this).data("id") 37 | }; 38 | 39 | // Reference number 40 | if($this.data("reference_number")){ 41 | post["reference_number"] = $this.data("reference_number"); 42 | } 43 | 44 | // Evitar que se pueda pulsar más de una vez sobre el botón 45 | if(this.clicked){ 46 | event.preventDefault(); 47 | return; 48 | } 49 | else{ 50 | this.clicked = true; 51 | } 52 | 53 | $this.addClass("waiting"); 54 | 55 | $.post(url, post, function(data){ 56 | var $input; 57 | 58 | if("status" in data && data["status"]=="error"){ 59 | show_error("Unable to create an operation number"); 60 | return false; 61 | } 62 | 63 | var formdata = data['data']; 64 | 65 | // Assignment of each attribute generated by server 66 | $form.attr({ 67 | "action": data['action'], 68 | "method": data['method'] 69 | }); 70 | 71 | if(data['enctype']){ 72 | $form.attr("enctype", data['enctype']); 73 | } 74 | 75 | for (name in formdata) { 76 | $input = $(''); 77 | $input.attr("name", name).val(formdata[name]); 78 | $form.append($input); 79 | } 80 | 81 | $form.submit(); 82 | 83 | return false; 84 | }); 85 | return false; 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /djangovirtualpos/templates/djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 13 | 14 | -------------------------------------------------------------------------------- /djangovirtualpos/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelligenia/django-virtual-pos/583abd4ed988738bcf6bd6a024478048160fe3d4/djangovirtualpos/templatetags/__init__.py -------------------------------------------------------------------------------- /djangovirtualpos/templatetags/djangovirtualpos_js.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from django import template 5 | from django.utils import timezone 6 | from django.template import loader, Context 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.simple_tag 12 | def include_djangovirtualpos_set_payment_attributes_js(set_payment_attributes_url, sale_code, url_ok, url_nok): 13 | t = loader.get_template("djangovirtualpos/templatetags/djangovirtualpos_js/djangovirtualpos_js.html") 14 | replacements = { 15 | "url_set_payment_attributes": set_payment_attributes_url, 16 | "sale_code": sale_code, 17 | "url_ok": url_ok, 18 | "url_nok": url_nok 19 | } 20 | try: 21 | return t.render(Context(replacements)) 22 | except TypeError: 23 | return t.render(replacements) -------------------------------------------------------------------------------- /djangovirtualpos/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | from django.conf.urls import url, include 6 | from django.conf.urls.static import static 7 | from django.contrib import admin 8 | 9 | 10 | from views import confirm_payment 11 | 12 | urlpatterns = [ 13 | url(r'^confirm/$', confirm_payment, name="confirm_payment") 14 | ] 15 | -------------------------------------------------------------------------------- /djangovirtualpos/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | from django.conf import settings 5 | from django.utils import timezone 6 | import pytz 7 | 8 | 9 | ######################################################################## 10 | ######################################################################## 11 | # Obtiene la dirección IP del cliente 12 | def get_client_ip(request): 13 | """ 14 | Obtiene la dirección IP del cliente. 15 | """ 16 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') 17 | if x_forwarded_for: 18 | return x_forwarded_for.split(',')[0] 19 | else: 20 | return request.META.get('REMOTE_ADDR') 21 | 22 | 23 | def as_server_datetime(_datetime): 24 | """Localiza la marca de tiempo en función de la zona de tiempo del servidor""" 25 | SERVERT_PYTZ = pytz.timezone(settings.TIME_ZONE) 26 | return SERVERT_PYTZ.localize(_datetime) 27 | 28 | 29 | def localize_datetime(_datetime): 30 | """Localiza la marca de tiempo en función de la zona de tiempo del servidor. Sólo y exclusivamente si no está localizada ya.""" 31 | if timezone.is_naive(_datetime): 32 | return as_server_datetime(_datetime) 33 | return _datetime 34 | 35 | 36 | def localize_datetime_from_format(str_datetime, datetime_format="%Y-%m-%d %H:%M"): 37 | _datetime = datetime.datetime.strptime(str_datetime, datetime_format) 38 | return localize_datetime(_datetime) 39 | 40 | 41 | ######################################################################## 42 | ######################################################################## 43 | 44 | 45 | def dictlist(node): 46 | res = {} 47 | res[node.tag] = [] 48 | xmltodict(node, res[node.tag]) 49 | reply = {} 50 | reply[node.tag] = {'value': res[node.tag], 'attribs': node.attrib, 'tail': node.tail} 51 | 52 | return reply 53 | 54 | 55 | def xmltodict(node, res): 56 | rep = {} 57 | if len(node): 58 | # n = 0 59 | for n in list(node): 60 | rep[node.tag] = [] 61 | value = xmltodict(n, rep[node.tag]) 62 | if len(n): 63 | value = {'value': rep[node.tag], 'attributes': n.attrib, 'tail': n.tail} 64 | res.append({n.tag: value}) 65 | else: 66 | res.append(rep[node.tag][0]) 67 | else: 68 | value = {} 69 | value = {'value': node.text, 'attributes': node.attrib, 'tail': node.tail} 70 | 71 | res.append({node.tag: value}) 72 | 73 | return 74 | -------------------------------------------------------------------------------- /djangovirtualpos/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.exceptions import ObjectDoesNotExist 6 | from django.db import transaction 7 | from django.http import Http404, HttpResponseRedirect 8 | from django.shortcuts import render, get_object_or_404 9 | from django.urls import reverse 10 | from django.views.decorators.csrf import csrf_exempt 11 | from djangovirtualpos.models import VirtualPointOfSale, VPOSCantCharge, VPOSRedsys 12 | 13 | 14 | from django.http import JsonResponse 15 | 16 | 17 | def set_payment_attributes(request, sale_model, sale_ok_url, sale_nok_url, reference_number=False): 18 | """ 19 | Set payment attributes for the form that makes the first call to the VPOS. 20 | :param request: HttpRequest. 21 | :param sale_model: Sale model. Must have "status" and "operation_number" attributes and "online_confirm" method. 22 | :param sale_ok_url: Name of the URL used to redirect client when sale is successful. 23 | :param sale_nok_url: Name of the URL used to redirect client when sale is not successful. 24 | :return: HttpResponse. 25 | """ 26 | if request.method == 'GET': 27 | return JsonResponse({"message":u"Method not valid."}) 28 | 29 | # Getting the VPOS and the Sale 30 | try: 31 | # Getting the VirtualPointOfSale object 32 | virtual_point_of_sale = VirtualPointOfSale.get(id=request.POST["vpos_id"], is_erased=False) 33 | # Getting Sale object 34 | payment_code = request.POST["payment_code"] 35 | sale = sale_model.objects.get(code=payment_code, status="pending") 36 | sale.virtual_point_of_sale = virtual_point_of_sale 37 | sale.save() 38 | 39 | except ObjectDoesNotExist as e: 40 | return JsonResponse({"message":u"La orden de pago no ha sido previamente creada."}, status=404) 41 | 42 | except VirtualPointOfSale.DoesNotExist: 43 | return JsonResponse({"message": u"VirtualPOS does NOT exist"}, status=404) 44 | 45 | virtual_point_of_sale.configurePayment( 46 | # Payment amount 47 | amount=sale.amount, 48 | # Payment description 49 | description=sale.description, 50 | # Sale code 51 | sale_code=sale.code, 52 | # Return URLs 53 | url_ok=request.build_absolute_uri(reverse(sale_ok_url, kwargs={"sale_code": sale.code})), 54 | url_nok=request.build_absolute_uri(reverse(sale_nok_url, kwargs={"sale_code": sale.code})), 55 | ) 56 | 57 | # Operation number assignment. This operation number depends on the 58 | # Virtual VPOS selected, it can be letters and numbers or numbers 59 | # or even match with a specific pattern depending on the 60 | # Virtual VPOS selected, remember. 61 | try: 62 | # Operation number generation and assignement 63 | operation_number = virtual_point_of_sale.setupPayment() 64 | # Update operation number of sale 65 | sale.operation_number = operation_number 66 | sale_model.objects.filter(id=sale.id).update(operation_number=operation_number) 67 | except Exception as e: 68 | return JsonResponse({"message": u"Error generating operation number {0}".format(e)}, status=500) 69 | 70 | # Payment form data 71 | if hasattr(reference_number, "lower") and reference_number.lower() == "request": 72 | form_data = virtual_point_of_sale.getPaymentFormData(reference_number="request") 73 | elif reference_number: 74 | form_data = virtual_point_of_sale.getPaymentFormData(reference_number=reference_number) 75 | else: 76 | form_data = virtual_point_of_sale.getPaymentFormData(reference_number=False) 77 | 78 | # Debug message 79 | form_data["message"] = "Payment {0} updated. Returning payment attributes.".format(payment_code) 80 | 81 | # Return JSON response 82 | return JsonResponse(form_data) 83 | 84 | 85 | # Confirm sale 86 | @csrf_exempt 87 | def confirm_payment(request, virtualpos_type, sale_model): 88 | """ 89 | This view will be called by the bank. 90 | """ 91 | # Checking if the Point of Sale exists 92 | virtual_pos = VirtualPointOfSale.receiveConfirmation(request, virtualpos_type=virtualpos_type) 93 | 94 | if not virtual_pos: 95 | # The VPOS does not exist, inform the bank with a cancel 96 | # response if needed 97 | return VirtualPointOfSale.staticResponseNok(virtualpos_type) 98 | 99 | # Verify if bank confirmation is indeed from the bank 100 | verified = virtual_pos.verifyConfirmation() 101 | operation_number = virtual_pos.operation.operation_number 102 | 103 | with transaction.atomic(): 104 | try: 105 | # Getting your payment object from operation number 106 | payment = sale_model.objects.get(operation_number=operation_number, status="pending") 107 | except ObjectDoesNotExist: 108 | return virtual_pos.responseNok("not_exists") 109 | 110 | if verified: 111 | # Charge the money and answer the bank confirmation 112 | # try: 113 | response = virtual_pos.charge() 114 | # Implement the online_confirm method in your payment 115 | # this method will mark this payment as paid and will 116 | # store the payment date and time. 117 | payment.virtual_pos = virtual_pos 118 | 119 | # Para el pago por referencia de Redsys 120 | reference_number = expiration_date = None 121 | if hasattr(virtual_pos, "delegated") and type(virtual_pos.delegated) == VPOSRedsys: 122 | print virtual_pos.delegated 123 | print virtual_pos.delegated.ds_merchantparameters 124 | reference_number = virtual_pos.delegated.ds_merchantparameters.get("Ds_Merchant_Identifier") 125 | expiration_date = virtual_pos.delegated.ds_merchantparameters.get("Ds_ExpiryDate") 126 | if reference_number: 127 | print(u"Online Confirm: Reference number") 128 | print(reference_number) 129 | payment.online_confirm(reference=reference_number, expiration_date=expiration_date) 130 | else: 131 | print(u"Online Confirm: No Reference number") 132 | payment.online_confirm() 133 | # except VPOSCantCharge as e: 134 | # return virtual_pos.responseNok(extended_status=e) 135 | # except Exception as e: 136 | # return virtual_pos.responseNok("cant_charge") 137 | 138 | else: 139 | # Payment could not be verified 140 | # signature is not right 141 | response = virtual_pos.responseNok("verification_error") 142 | 143 | return response 144 | -------------------------------------------------------------------------------- /manual/COMMON.md: -------------------------------------------------------------------------------- 1 | TPVs 2 | ==== 3 | 4 | Manual de implantación de TPV Virtual para comercios 5 | ---------------------------------------------------- 6 | 7 | # Módulo genérico de TPVs 8 | 9 | El proceso de compra tendrá tres pasos (el paso 2 es opcional): 10 | 11 | --- 12 | 13 | ## Diagrama de flujo (PASO 1) 14 | 15 | 16 | PASO a) 17 | +-----------+ ----------------------> +------------+ 18 | | CLIENTE | | SERVIDOR | 19 | |-----------| |------------| 20 | | | PASO b) | | 21 | | | <----------------------- | | 22 | +-----------+ +------------+ 23 | | 24 | | 25 | | 26 | | PASO c) 27 | | +-------------+ 28 | | | PASARELA | 29 | +---------> +-------------+ 30 | 31 | 32 | --- 33 | 34 | ### Paso 1. Envío a la pasarela de datos 35 | 36 | 37 | #### Paso a). Configuración del pago 38 | a.1. Se prepara la URL del pago según el entorno, el importe y el idioma. 39 | ```python 40 | def configurePayment(self, amount, description, url_ok, url_nok, sale_code): 41 | """ 42 | Configura el pago por TPV. 43 | Prepara el objeto TPV para: 44 | - Pagar una cantidad concreta 45 | - Establecera una descripción al pago 46 | - Establecer las URLs de OK y NOK 47 | - Almacenar el código de venta de la operación 48 | """ 49 | ``` 50 | > Después de realizar la configuración del método general, se llamará a las configuraciones específicas, a través de los delegados. 51 | 52 | a.2. Se prepara el importe con el formato que solicite la entidad bancaria. 53 | ```python 54 | def setupPayment(self): 55 | """ 56 | Prepara el TPV. 57 | Genera el número de operación y prepara el proceso de pago. 58 | """ 59 | ``` 60 | > Comprobamos que el número de operación generado por el delegado es único en la tabla de TpvPaymentOperation 61 | 62 | a.3. Se obtienen los datos del pago rellenando el formulario con los datos rellenados por el cliente. Luego se enviará el formulario por POST a través de Javascript. 63 | ```python 64 | def getPaymentFormData(self): 65 | """ 66 | Este método será el que genere los campos del formulario de pago 67 | que se rellenarán desde el cliente (por Javascript) 68 | """ 69 | ``` 70 | > La generación de los campos de los formularios será específica de cada TPV a través de una llamada de su delegado 71 | 72 | #### Paso b). El servidor renderiza el formulario y lo devuelve al cliente para que lo envíe a la pasarela de pago. 73 | 74 | #### Paso c). El cliente envia mediante un formulario POST a la pasarela de pago los datos que solicita la entidad bancaria para realizar la operación. 75 | 76 | --- 77 | 78 | ## Diagrama de flujo (PASO 3) 79 | 80 | PASO a) 81 | +-----------+ ----------------------> +------------+ 82 | | PASARELA | | SERVIDOR | 83 | |-----------| |------------| 84 | | | PASO b) | | 85 | | | <----------------------- | | 86 | +-----------+ +------------+ 87 | | 88 | | 89 | | 90 | | PASO c) 91 | | +-------------+ 92 | | | CLIENTE | 93 | +---------> +-------------+ 94 | 95 | --- 96 | 97 | ### Paso 3. Confirmación del pago 98 | 99 | > En este paso se encarga de la comunicación con la pasarela de pago y la verificación de los datos 100 | 101 | #### Paso a). Obtenemos número de operación y datos 102 | Guardamos el número de operación y el diccionario enviado desde la pasarela de pago. Cada pasarela de pago enviará los datos de distinta manera, por lo que hay que hacer recepciones específicas. 103 | ```python 104 | def receiveConfirmation(request, tpv_type): 105 | """ 106 | Este método se encargará recibir la información proporcionada por el tpv, 107 | debido a que cada información recibida variará segun el TPV, directamente llamará a cada delegado, según el tipo de tpv. 108 | """ 109 | ``` 110 | #### Paso b). El servidor verifica que los datos enviados desde la pasarela de pago identifiquen a una operación de compra. Dependiendo si la verificación es correcta o no, se enviará URLOK o URLNOK 111 | Se calcula la firma y se compara con la que ha generado la pasarela de pago 112 | ```python 113 | def verifyConfirmation(self): 114 | """ 115 | Este método también se encargará de llamar a cada método delegado de tpv, se creará la firma y se comprobará que 116 | coincida con la enviada por la pasarela de pago 117 | """ 118 | ``` 119 | #### Paso 3. La pasarela de pago redirige al cliente a una URL. Dependiendo si ha ido bien o mal, redirige a una o a otra. Si la firma ha sido verificada el Servidor mandará un correo electrónico al cliente. 120 | > Si la respuesta ha ido bien, se utilizará el siguiente método: 121 | 122 | ```python 123 | def charge(self): 124 | """ 125 | Última comunicación con el TPV (si hiciera falta).Esta comunicación sólo se realiza en 126 | PayPal, dado que en CECA y otros hay una verificación y una respuesta con "OK" 127 | """ 128 | ``` 129 | 130 | > Si ha habido un error en el pago, se ha de dar una respuesta negativa a la pasarela bancaria. 131 | 132 | ```python 133 | def responseNok(self): 134 | dlprint("responseNok") 135 | return HttpResponse("") 136 | ``` 137 | 138 | 139 | 140 | Especificaciones particulares de cada TPV 141 | ----------------------------------------- 142 | 143 | > Cada TPV tiene una manera distinta, tanto de enviar los datos como de recibirlos, a continuación detallamos los campos: 144 | 145 | - [CECA](manual/vpos_types/CECA.md) 146 | - [PAYPAL](manual/vpos_types/PAYPAL.md) 147 | - [REDSYS](manual/vpos_types/REDSYS.md) 148 | - [BITPAY](manual/vpos_types/BITPAY.md) 149 | - Santander Enlavon está pendiente. 150 | -------------------------------------------------------------------------------- /manual/vpos_types/BITPAY.md: -------------------------------------------------------------------------------- 1 | # Biptay 2 | 3 | ##### Configuración de la operación 4 | 5 | ```python 6 | def setupPayment(self, code_len=40): 7 | ``` 8 | 9 | Realiza una petición a Bitpay, ``Create an Invoice``. 10 | 11 | Los parámetros que se deben incorporar pare crear una orden de pago en bitpay son los siguientes (* son obligatórios). 12 | 13 | 14 | | Name | Description | 15 | |-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 16 | | price* | This is the amount that is required to be collected from the buyer. Note, if this is specified in a currency other than BTC, the price will be converted into BTC at market exchange rates to determine the amount collected from the buyer. | 17 | | currency* | This is the currency code set for the price setting. The pricing currencies currently supported are USD, EUR, BTC, and all of the codes listed on this page: ​https://bitpay.com/bitcoin­exchange­rates | 18 | | posData | A passthrough variable provided by the merchant and designed to be used by the merchant to correlate the invoice with an order or other object in their system. Maximum string length is 100 characters.,This passthrough variable can be a JSON­encoded string, for example,posData: ‘ ``{ “ref” : 711454, “affiliate” : “spring112” }`` ‘ | 19 | | notificationURL | A URL to send status update messages to your server (this must be an https URL, unencrypted http URLs or any other type of URL is not supported).,Bitpay.com will send a POST request with a JSON encoding of the invoice to this URL when the invoice status changes. | 20 | | transactionSpeed | default value: set in your ​https://bitpay.com/order­settings​, the default value set in your merchant dashboard is “medium”. - **high**: An invoice is considered to be "confirmed" immediately upon receipt of payment., - **medium**: An invoice is considered to be "confirmed" after 1 blockconfirmation (~10 minutes). - **low**: An invoice is considered to be "confirmed" after 6 block confirmations (~1 hour). | 21 | | fullNotifications | Notifications will be sent on every status change. | 22 | | notificationEmail | Bitpay will send an email to this email address when the invoice status changes. | 23 | | redirectURL | This is the URL for a return link that is displayed on the receipt, to return the shopper back to your website after a successful purchase. This could be a page specific to the order, or to their account. | 24 | 25 | > En nuetra implementación particular incorporamos los siguientes campos: 26 | 27 | ```python 28 | 29 | params = { 30 | 'price': self.importe, 31 | 'currency': self.currency, 32 | 'redirectURL': self.parent.operation.url_ok, 33 | 'itemDesc': self.parent.operation.description, 34 | 'notificationURL': self.notification_url, 35 | # Campos libres para el programador, puedes introducir cualquier información útil. 36 | # En nuestro caso el prefijo de la operación, que ayuda a TPV proxy a identificar el servidor 37 | # desde donde se ha ejecutado la operación. 38 | 'posData': json.dumps({"operation_number_prefix": self.operation_number_prefix}) 39 | 'fullNotifications': True 40 | } 41 | 42 | ``` 43 | 44 | >Tales parámetros se envían como ``JSON`` con el verbo http ``POST``. 45 | 46 | >Como resultado de esta petición obtenemos una respuesta JSON como la siguiente: 47 | 48 | ```json 49 | { 50 | "status":"new", 51 | "btcPaid":"0.000000", 52 | "invoiceTime":1518093126310, 53 | "buyerFields":{ }, 54 | "currentTime":1518093126390, 55 | "url":"https://bitpay.com/invoice?id=X7VytgMABGuv5Vo4xPsRhb", 56 | "price":5, 57 | "btcDue":"0.000889", 58 | "btcPrice":"0.000721", 59 | "currency":"EUR", 60 | "rate":6938.79, 61 | "paymentSubtotals":{ 62 | "BTC":72100 63 | }, 64 | "paymentTotals":{ 65 | "BTC":88900 66 | }, 67 | "expirationTime":1518094026310, 68 | "id":"X7VytgMABGuv5Vo4xPsRhb", 69 | "exceptionStatus": false 70 | } 71 | ``` 72 | 73 | >De esta petición capturamos el ``id`` para almacenarlo como identificador de la operación. 74 | 75 | --- 76 | 77 | ##### Generación de los datos de pago que se enviarán a la pasarela 78 | 79 | ```python 80 | def getPaymentFormData(self): 81 | ``` 82 | 83 | >Dada la URL principal de bitpay (entorno test o estable) y el identificador de la operación, construimos un formulario ``GET`` con referencia a la url de pago de bitpay. 84 | 85 | >Al hacer **submit** de este formulario el usuario es redirigido a la plataforma de Bitpay y cuando termine la operación vuelve a la aplicación, (a la url de vuelta establecida). 86 | 87 | --- 88 | 89 | ##### Obtención de los datos de pago enviados por la pasarela 90 | 91 | ```python 92 | def receiveConfirmation(request): 93 | ``` 94 | 95 | >Cuando se produce un cambio de estado en la operación de pago realizada anteriormente, el servidor de bitpay hace una petición POST a la url ``confirm/``, indicando el nuevo estado. Este método tiene la responsabilidad de identificar la operación dedo el ``id`` y capturar el ``status``. 96 | 97 | --- 98 | 99 | 100 | ##### Verificación de la operación 101 | 102 | ```python 103 | def verifyConfirmation(self): 104 | ``` 105 | 106 | >Tiene la responsabilidad de verificar que el nuevo estado comunicado en ``receiveConfirmation`` se corresponde con **"confirmed"**, ello quiere decir que el pago ha sido escrito correctamente en blockchain. 107 | --- 108 | 109 | ##### Confirmación de la operación 110 | 111 | ```python 112 | def charge(self): 113 | ``` 114 | 115 | En Bitpay no es importante la respuesta de la url ``confirm/``, siempre y cuando sea un 200, por tanto nosotros enviamos el string **"OK"** 116 | 117 | -------------------------------------------------------------------------------- /manual/vpos_types/CECA.md: -------------------------------------------------------------------------------- 1 | # CECA 2 | 3 | ##### Generación del número de operación 4 | 5 | ```python 6 | def setupPayment(self, code_len=40): 7 | ``` 8 | 9 | > El número de operación para CECA se genera anteponiendole un prefijo (el usuario lo podrá editar desde la administración) 10 | > A continuación un código aleatorio alfanumerico con una longitud de 40 caracteres 11 | 12 | --- 13 | 14 | ##### Generador de firma para el envío 15 | 16 | ```python 17 | def _sent_signature(self): 18 | ``` 19 | 20 | > Este método calcula la firma a incorporar en el formulario de pago 21 | > Se encriptarán a través de una clave de cifrado la siguiente cadena: 22 | 23 | {encryption_key}{merchant_id}{acquirer_bin}{terminal_id}{num_operacion}{importe}{tipo_moneda}{exponente}SHA1{url_ok}{url_nok} 24 | 25 | --- 26 | 27 | ##### Generación de los datos de pago que se enviarán a la pasarela 28 | 29 | ```python 30 | def getPaymentFormData(self): 31 | ``` 32 | 33 | > Este formulario enviará los campos siguientes: 34 | 35 | **MerchantID:** Identifica al comercio, será facilitado por la caja 36 | **AcquirerID:** Identifica a la caja, será facilitado por la caja 37 | **TerminalID:** Identifica al terminal, será facilitado por la caja 38 | **Num_operacion:** Identifica el número de pedido, factura, albarán, etc 39 | **Importe:** Importe de la operación sin formatear. Siempre será entero con los dos últimos dígitos usados para los centimos 40 | **TipoMoneda:** Codigo ISO-4217 correspondiente a la moneda en la que se efectúa el pago 41 | **Exponente:** Actualmente siempre será 2 42 | **URL_OK:** URL determinada por el comercio a la que CECA devolverá el control en caso de que la operación finalice correctamente 43 | **URL_NOK:** URL determinada por el comercio a la que CECA devolverá el control en caso de que la operación NO finalice correctamente 44 | **Firma:** Cadena de caracteres calculada por el comercio 45 | **Cifrado:** Tipo de cifrado que se usará para el cifrado de la firma 46 | **Idioma:** Código de idioma 47 | **Pago_soportado:** Valor fijo: SSL 48 | **Descripcion:** Opcional. Campo reservado para mostrar información en la página de pago 49 | 50 | --- 51 | 52 | ##### Obtención de los datos de pago enviados por la pasarela 53 | 54 | ```python 55 | def receiveConfirmation(request): 56 | ``` 57 | 58 | > Estos serán los valores que nos devuelve la pasarela de pago 59 | 60 | **MerchantID:** Identifica al comercio 61 | **AcquirerBIN:** Identifica a la caja 62 | **TerminalID:** Identifica al terminal 63 | **Num_operacion:** Identifica el número de pedido, factura, albarán, etc 64 | **Importe:** Importe de la operación sin formatear 65 | **TipoMoneda:** Corresponde a la moneda en la que se efectúa el pago 66 | **Exponente:** Actualmente siempre será 2 67 | **Idioma:** Idioma de la operación 68 | **Pais:** Código ISO del país de la tarjeta que ha realizado la operación 69 | **Descripcion:** Los 200 primeros caracteres de la operación 70 | **Referencia:** Valor único devuelto por la pasarela. Imprescinfible para realizar cualquier tipo de reclamanción y/o anulación 71 | **Num_aut:** Valor asignado por la entidad emisora a la hora de autorizar una operación 72 | **Firma:** Es una cadena de caracteres calculada por CECA firmada por SHA1 73 | 74 | --- 75 | 76 | 77 | ##### Verificación de la operación 78 | 79 | ```python 80 | def _verification_signature(self): 81 | ``` 82 | 83 | > Este método calcula la firma que más tarde se comparará con la enviada por la pasarela 84 | > Se encriptarán a través de una clave de cifrado la siguiente cadena: 85 | 86 | {encryption_key}{merchant_id}{acquirer_bin}{terminal_id}{num_operacion}{importe}{tipo_moneda}{exponente}{referencia} 87 | 88 | > La 'Referencia' será un valor que devolvió la pasarela 89 | 90 | --- 91 | 92 | ##### Confirmación de la operación 93 | 94 | ```python 95 | def charge(self): 96 | ``` 97 | 98 | > En el TPV de CECA habrá que enviar una respuesta, para que la pasarela proceda a enviar la confirmación al cliente 99 | > El valor que habrá que enviar es "$*$OKY$*$" 100 | 101 | -------------------------------------------------------------------------------- /manual/vpos_types/PAYPAL.md: -------------------------------------------------------------------------------- 1 | # PAYPAL 2 | 3 | --- 4 | 5 | > **Importante:** El envío y recepción de datos en PayPal es distinto al del resto de TPVs. Nosotros enviaremos los datos 6 | > desde un formulario a través de POST y recibiremos desde PayPal a través de GET. Al contrario que el resto de TPVs, con 7 | > PayPal no es el cliente el que envía los formularios, sino que los envía el servidor. 8 | > El siguiente diagrama muestra el flujo de ejecución de Express Checkout, los cuadros de la izquierda representan 9 | > nuestro website y los de la derecha el servidor de PayPal. 10 | 11 | --- 12 | 13 | ## Diagrama de flujo 14 | 15 | PASO 1 16 | +-----------+ ----------------------> +------------+ 17 | | Finalizar | | SERVIDOR | 18 | | Compra | PASO 2 | PAYPAL | 19 | | | <----------------------- | | 20 | +-----------+ +------------+ 21 | | 22 | | 23 | | 24 | | PASO 3 25 | | +-------------+ 26 | | | Log In | 27 | | | PAYPAL | 28 | +-------------------------------> +-------------+ 29 | | 30 | | 31 | | Paso 4 32 | | 33 | v 34 | +------------+ +------------+ 35 | | Recibimos | PASO 5 | Continuar | 36 | |Confirmacion| <----------------------- | PAYPAL | 37 | | | | | 38 | +------------+ +------------+ 39 | | 40 | | 41 | | 42 | | PASO 6 43 | | +-------------+ 44 | | | SERVIDOR | 45 | | | PAYPAL | 46 | +--------------------------------> +-------------+ 47 | | 48 | | 49 | +------------+ | 50 | | | PASO 7 | 51 | |Confirmacion| <------------------------------+ 52 | | | 53 | +------------+ 54 | 55 | --- 56 | 57 | ### Pasos: 58 | **1.** Peticion al servidor de PayPal: SetExpressCheckout 59 | 1.1. Se le pasan los datos API_Username, API_Password, API_Signature, importe, PAYMENTREQUEST_0_PAYMENTACTION=SALE, 60 | la version=95, RETURN URL (url a la que vuelve si va todo bien), CANCELURL (url a la que redirige si hay error) 61 | **2.** Respuesta de PayPal con el Token 62 | 2.1. Se guarda el token, ya que será usado como el número de operación 63 | **3.** Redirige a Paypal para logearse 64 | **4.** Se redirige al usuario a la pagina de Login de Paypal 65 | **5.** PayPal confirma el token y el PayerID 66 | 5.1. Después de logearse, se redirige a la RETURNURL con el token y el PayerID 67 | **6.** Enviamos DoExpressCheckoutPayment con el token y PayerID 68 | **7.** PayPal confirma el pago, y según falle o no manda a la URL 69 | 70 | --- 71 | 72 | ##### Configuración del pago 73 | 74 | ```python 75 | def configurePayment(self): 76 | ``` 77 | 78 | > En PayPal solo debemos configurar el importe, no hay idioma. 79 | > El importe debe ser el valor de la operación y dos digitos decimales separador por un punto (.) 80 | > Un ejemplo para una venta de 15€ seria: 15.00 81 | 82 | --- 83 | 84 | 85 | ##### Generación del número de operación 86 | 87 | ```python 88 | def setupPayment(self, code_len=12): 89 | ``` 90 | 91 | > El número de operación para PayPal no se genera como en los demás, sino que será un token devuelto 92 | > por PayPal, por tanto, en este paso preparamos un formulario con los siguientes datos: 93 | 94 | **METHOD:** Indica el método a usar (SetExpressCheckout) 95 | **VERSION:** Indica la versión (95 en este caso) 96 | **USER:** Indica el usuario registrado con cuenta bussiness en paypal 97 | **PWD:** Indica la contraseña del usuario registrado con cuenta bussiness en paypal 98 | **SIGNATURE:** Indica la firma del usuario registrado con cuenta bussiness en paypal 99 | **PAYMENTREQUEST_0_AMT:** Importe de la venta 100 | **PAYMENTREQUEST_0_CURRENCYCODE:** ID de la moneda a utilizar 101 | **RETURNURL:** URL donde Paypal redirige al usuario comprador después de logearse en Paypal 102 | **CANCELURL:** URL a la que Paypal redirige al comprador si el comprador no aprueba el pago 103 | **PAYMENTREQUEST_0_PAYMENTACTION:** Especifíca la acción 104 | 105 | > Estos dos últimos valores se pasan a PayPal para que los muestre en el resumen de la venta 106 | 107 | **L_PAYMENTREQUEST_0_NAME0:** Especifica la descripción de la venta 108 | **L_PAYMENTREQUEST_0_AMT0:** Especifica el importe final de la venta 109 | 110 | > Una vez enviado el formulario, comprobamos en este mismo método la respuesta. Ésta debe contener 111 | > un ACK con el valor de Success, y un TOKEN 112 | 113 | --- 114 | 115 | ##### Generador de firma para el envío 116 | 117 | > En PayPal no es necesaria la firma para el envío, ya que se hace a través de los datos 118 | > de la API proporcionados en el formulario anterior 119 | 120 | --- 121 | 122 | ##### Generación de los datos de pago que se enviarán a la pasarela 123 | 124 | 125 | ```python 126 | def getPaymentFormData(self): 127 | ``` 128 | 129 | > Este formulario enviará los campos siguientes: 130 | 131 | **cmd:** Indica el tipo de operación que vamos a usar, en este caso "_express-checkout" 132 | **token:** Indica el token devuelto por PayPal en el paso anterior 133 | 134 | --- 135 | 136 | ##### Obtención de los datos de pago enviados por la pasarela 137 | 138 | ```python 139 | def receiveConfirmation(request): 140 | ``` 141 | 142 | > Estos serán los valores que nos devuelve la pasarela de pago 143 | 144 | **PayerID:** Número de identificación del comprador 145 | **token:** Número de operación 146 | 147 | 148 | --- 149 | 150 | ##### Verificación de la operación 151 | 152 | ```python 153 | def _verification_signature(self): 154 | ``` 155 | 156 | > Este método lo único que hará será comprobar que, en la tabla TPVPaymentOperation exista 157 | > un número de operación que coincida con el token devuelto por PayPal 158 | 159 | --- 160 | 161 | ##### Confirmación de la operación 162 | 163 | ```python 164 | def charge(self): 165 | ``` 166 | 167 | > En el TPV de Paypal, éste método enviará por POST un formulario con solo siguientes campos: 168 | 169 | **METHOD:** Indica el método a usar, en este caso "DoExpressCheckoutPayment" 170 | **USER:** Indica el nombre del usuario con cuenta bussiness 171 | **PWD:** Indica la contraseña del usuario con cuenta bussiness 172 | **SIGNATURE:** Indica la firma del usuario con cuenta bussiness 173 | **VERSION:** Indica la versión de PayPal, en este caso la 95 174 | **TOKEN:** Indica el token (número de operación) 175 | **PAYERID:** Indica el valor del número de identificación del usuario comprador 176 | **PAYMENTREQUEST_0_CURRENCYCODE:** Indica el tipo de moneda 177 | **PAYMENTREQUEST_0_PAYMENTACTION:** Indica el tipo de acción, en este caso es "Sale" 178 | **PAYMENTREQUEST_0_AMT:** Indica el importe final de la venta 179 | 180 | > Este método recibirá mediante GET el token. 181 | 182 | > Al enviar este formulario recibiremos una respuesta de PayPal, en la que debemos comprobar 183 | > si existe ACK y que su valor sea "Success" y si existe un token. 184 | -------------------------------------------------------------------------------- /manual/vpos_types/REDSYS.md: -------------------------------------------------------------------------------- 1 | # REDSYS 2 | 3 | ##### Generación del número de operación 4 | 5 | ```python 6 | def setupPayment(self, code_len=12): 7 | ``` 8 | 9 | > El número de operación para REDSYS se genera anteponiendole un prefijo (el usuario lo podrá editar desde la administración) 10 | > A continuación un código aleatorio de 4 números (entre 2 y 9) y una cadena alfanumerica (en mayúsculas) con una longitud de 8 caracteres 11 | > Un ejemplo seria: 2645S5A8D88W 12 | 13 | --- 14 | 15 | ##### Generador de firma para el envío 16 | 17 | ```python 18 | def _sent_signature(self): 19 | ``` 20 | 21 | > Este método calcula la firma a incorporar en el formulario de pago 22 | > Se encriptarán a través de una clave de cifrado la siguiente cadena: 23 | 24 | {importe}{num_pedido}{merchant_code}{tipo_moneda}{transaction_type}{merchant_url}{encryption_key} 25 | 26 | --- 27 | 28 | ##### Generación de los datos de pago que se enviarán a la pasarela 29 | 30 | ```python 31 | def getPaymentFormData(self): 32 | ``` 33 | 34 | > Este formulario enviará los campos siguientes: 35 | 36 | **Ds_Merchant_Amount:** Indica el importe de la venta 37 | **Ds_Merchant_Currency:** Indica el tipo de moneda a usar 38 | **Ds_Merchant_Order:** Indica el número de operacion 39 | **Ds_Merchant_ProductDescription:** Se mostrará al titular en la pantalla de confirmación de la compra 40 | **Ds_Merchant_MerchantCode:** Código FUC asignado al comercio 41 | **Ds_Merchant_UrlOK:** URL a la que se redirige al usuario en caso de que la venta haya sido satisfactoria 42 | **Ds_Merchant_UrlKO:** URL a la que se redirige al usuario en caso de que la venta NO haya sido satisfactoria 43 | **Ds_Merchant_MerchantURL:** Obligatorio si se tiene confirmación online. 44 | **Ds_Merchant_ConsumerLanguage:** Indica el valor del idioma 45 | **Ds_Merchant_MerchantSignature:** Indica la firma generada por el comercio 46 | **Ds_Merchant_Terminal:** Indica el terminal 47 | **Ds_Merchant_SumTotal:** Representa la suma total de los importes de las cuotas 48 | **Ds_Merchant_TransactionType:** Indica que tipo de transacción se utiliza 49 | 50 | --- 51 | 52 | ##### Obtención de los datos de pago enviados por la pasarela 53 | 54 | ```python 55 | def receiveConfirmation(request): 56 | ``` 57 | 58 | > Estos serán los valores que nos devuelve la pasarela de pago 59 | 60 | - **Ds_Date:** Fecha de la transacción 61 | - **Ds_Hour:** Hora de la transacción 62 | - **Ds_Amount:** Importe de la venta, mismo valor que en la petición 63 | - **Ds_Currency:** Tipo de moneda 64 | - **Ds_Order:** Número de operación, mismo valor que en la petición 65 | - **Ds_MerchantCode:** Indica el código FUC del comercio, mismo valor que en la petición 66 | - **Ds_Terminal:** Indica la terminal, , mismo valor que en la petición 67 | - **Ds_Signature:** Firma enviada por RedSys, que más tarde compararemos con la generada por el comercio 68 | - **Ds_Response:** Código que indica el tipo de transacción 69 | - **Ds_MerchantData:** Información opcional enviada por el comercio en el formulario de pago 70 | - **Ds_SecurePayment:** Indica: 0, si el pago es NO seguro; 1, si el pago es seguro 71 | - **Ds_TransactionType:** Tipo de operación que se envió en el formulario de pago 72 | - **Card_Country:** País de emisión de la tarjeta con la que se ha intentado realizar el pago 73 | - **AuthorisationCode:** Código alfanumerico de autorización asignado a la aprobación de la transacción 74 | - **ConsumerLanguage:** El valor 0 indicará que no se ha determinado el idioma 75 | - **Card_type:** Valores posibles: C - Crédito; D - Débito 76 | 77 | --- 78 | 79 | ##### Verificación de la operación 80 | 81 | ```python 82 | def _verification_signature(self): 83 | ``` 84 | 85 | > Este método calcula la firma que más tarde se comparará con la enviada por la pasarela 86 | > Se encriptarán a través de una clave de cifrado la siguiente cadena: 87 | 88 | {importe}{merchant_order}{merchant_code}{tipo_moneda}{ds_response}{encription_key} 89 | 90 | > El campo 'ds_response' será un valor que devolvió la pasarela 91 | 92 | --- 93 | 94 | ##### Confirmación de la operación 95 | 96 | ```python 97 | def charge(self): 98 | ``` 99 | 100 | > En el TPV de REDSYS no habrá que enviar una respuesta como en CECA, de todas maneras nosotros enviamos una cadena vacia 101 | 102 | 103 | ##### Devolución o reembolso de un pago. 104 | 105 | ```python 106 | def refund(self, operation_sale_code, refund_amount, description): 107 | ``` 108 | 109 | > Para ello se prepara una petición http con los siguientes campos: 110 | 111 | - **Ds_Merchant_Amount:** Indica el importe de la devolución 112 | - **Ds_Merchant_Currency:** Indica el tipo de moneda a usar 113 | - **Ds_Merchant_Order:** Indica el número de operacion, (que debe coincidir con el número de operación del pago realizado). 114 | - **Ds_Merchant_ProductDescription:** Descripción de la devolución. 115 | - **Ds_Merchant_MerchantCode:** Código FUC asignado al comercio 116 | - **Ds_Merchant_UrlOK:** URL a la que se redirige al usuario en caso de operación OK (no es muy importante dado que al ser un proceso en backend no se llega a producir tal redicrección). 117 | - **Ds_Merchant_UrlKO:** URL a la que se redirige al usuario en caso de que la operación NO haya sido satisfactoria, (misma circunstancia que la anterior) 118 | - **Ds_Merchant_MerchantURL:** Obligatorio si se tiene confirmación online. 119 | - **Ds_Merchant_ConsumerLanguage:** Indica el valor del idioma 120 | - **Ds_Merchant_MerchantSignature:** Indica la firma generada por el comercio 121 | - **Ds_Merchant_Terminal:** Indica el terminal 122 | - **Ds_Merchant_SumTotal:** Representa la suma total de la devolución 123 | - **Ds_Merchant_TransactionType:** Indica que tipo de transacción se utiliza, (para devoluciones usar **3**). 124 | 125 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says to generate wheels that support both Python 2 and Python 3 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 4 | # need to generate separate wheels for each Python version that you 5 | # support. 6 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2015 by intelligenia 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Copyright (c) 2016 intelligenia soluciones informáticas 8 | 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | 27 | from setuptools import setup, Command, find_packages 28 | from shutil import rmtree 29 | import os 30 | import sys 31 | import io 32 | 33 | # Package meta-data. 34 | NAME = "django-virtual-pos" 35 | DESCRIPTION = "django-virtual-pos is a module that abstracts the flow of paying in several online payment platforms." 36 | URL = 'https://github.com/intelligenia/django-virtual-pos' 37 | EMAIL = 'mario@intelligenia.com' 38 | AUTHOR = 'intelligenia' 39 | REQUIRES_PYTHON = '>=2.7.0' 40 | VERSION = None 41 | KEYWORDS = ["virtual", "point-of-sale", "puchases", "online", "payments"] 42 | 43 | # Directory with the package 44 | PACKAGE = "djangovirtualpos" 45 | 46 | # What packages are required for this module to be executed? 47 | REQUIRED = [ 48 | "django", 49 | "beautifulsoup4", 50 | "lxml", 51 | "pycrypto", 52 | "pytz", 53 | "requests" 54 | ] 55 | 56 | # The rest you shouldn't have to touch too much :) 57 | # ------------------------------------------------ 58 | # Except, perhaps the License and Trove Classifiers! 59 | # If you do change the License, remember to change the Trove Classifier for that! 60 | 61 | here = os.path.abspath(os.path.dirname(__file__)) 62 | 63 | # Import the README and use it as the long-description. 64 | # Note: this will only work if 'README.rst' is present in your MANIFEST.in file! 65 | with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 66 | long_description = '\n' + f.read() 67 | 68 | # Load the package's __version__.py module as a dictionary. 69 | about = {} 70 | if not VERSION: 71 | with open(os.path.join(here, PACKAGE, '__version__.py')) as f: 72 | exec (f.read(), about) 73 | else: 74 | about['__version__'] = VERSION 75 | 76 | 77 | class UploadCommand(Command): 78 | """Support setup.py upload.""" 79 | 80 | description = 'Build and publish the package.' 81 | user_options = [] 82 | 83 | @staticmethod 84 | def status(s): 85 | """Prints things in bold.""" 86 | print('\033[1m{0}\033[0m'.format(s)) 87 | 88 | def initialize_options(self): 89 | pass 90 | 91 | def finalize_options(self): 92 | pass 93 | 94 | def run(self): 95 | try: 96 | self.status('Removing previous builds…') 97 | rmtree(os.path.join(here, 'dist')) 98 | except OSError: 99 | pass 100 | 101 | self.status('Building Source and Wheel (universal) distribution…') 102 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 103 | 104 | self.status('Uploading the package to PyPi via Twine…') 105 | os.system('twine upload dist/*') 106 | 107 | self.status('Pushing git tags…') 108 | os.system('git tag v{0}'.format(about['__version__'])) 109 | os.system('git push --tags') 110 | 111 | sys.exit() 112 | 113 | 114 | setup( 115 | name=NAME, 116 | version=about['__version__'], 117 | description=DESCRIPTION, 118 | long_description=long_description, 119 | author=AUTHOR, 120 | author_email=EMAIL, 121 | python_requires=REQUIRES_PYTHON, 122 | url=URL, 123 | packages=find_packages(exclude=('tests',)), 124 | # If your package is a single module, use this instead of 'packages': 125 | # py_modules=['mypackage'], 126 | 127 | # entry_points={ 128 | # 'console_scripts': ['mycli=mymodule:cli'], 129 | # }, 130 | install_requires=REQUIRED, 131 | include_package_data=True, 132 | license="MIT", 133 | classifiers=[ 134 | 'Development Status :: 5 - Production/Stable', 135 | 'Framework :: Django', 136 | 'License :: OSI Approved :: MIT License', 137 | ], 138 | keywords=KEYWORDS, 139 | cmdclass={ 140 | 'upload': UploadCommand, 141 | }, 142 | ) 143 | --------------------------------------------------------------------------------