├── test ├── __init__.py ├── test_tpe_autoget.py ├── test_payment.py └── test_tpe.py ├── dev-requirements.txt ├── docs ├── requirements.txt ├── installation.rst ├── requirements.rst ├── Makefile ├── exceptions.rst ├── index.rst ├── constants.rst ├── examples.rst ├── conf.py └── classes.rst ├── telium ├── version.py ├── __init__.py ├── hexdump.py ├── constant.py ├── manager.py └── payment.py ├── setup.cfg ├── .github └── workflows │ ├── run-tests.yml │ └── python-publish.yml ├── LICENSE ├── .gitignore ├── setup.py ├── CODE_OF_CONDUCT.md └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | Faker 2 | pytest 3 | pytest-cov 4 | codecov 5 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | pyserial 3 | pycountry>=17.0 4 | 5 | -------------------------------------------------------------------------------- /telium/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Expose version 3 | """ 4 | 5 | __version__ = "2.4.2" 6 | VERSION = __version__.split('.') 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov=telium --cov-report=term-missing -rxXs 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /telium/__init__.py: -------------------------------------------------------------------------------- 1 | from telium.constant import * 2 | from telium.payment import TeliumAsk, TeliumResponse, LrcChecksumException, SequenceDoesNotMatchLengthException 3 | from telium.manager import * 4 | from telium.version import __version__, VERSION 5 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | This installs a package that can be used from Python (``import telium``). 5 | 6 | To install for all users on the system, administrator rights (root) 7 | may be required. 8 | 9 | From PyPI 10 | --------- 11 | pyTeliumManager can be installed from PyPI:: 12 | 13 | pip install pyTeliumManager 14 | 15 | From git via dev-master 16 | ----------------------- 17 | You can install from dev-master branch using git:: 18 | 19 | git clone https://github.com/Ousret/pyTeliumManager.git 20 | cd pyTeliumManager/ 21 | python setup.py install 22 | -------------------------------------------------------------------------------- /test/test_tpe_autoget.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main 2 | from telium import * 3 | from os.path import exists 4 | 5 | 6 | class TestTPEAutoGet(TestCase): 7 | 8 | def test_auto_get(self): 9 | 10 | for path in TERMINAL_PROBABLES_PATH: 11 | if exists(path): 12 | # Should not render None 13 | my_telium_instance = Telium.get() 14 | self.assertIsNotNone(my_telium_instance) 15 | my_telium_instance.close() 16 | return 17 | 18 | self.assertIsNone(Telium.get()) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() -------------------------------------------------------------------------------- /docs/requirements.rst: -------------------------------------------------------------------------------- 1 | Requirements 2 | ============ 3 | 4 | This package is intended to be cross-platform. Unix, Linux and NT systems are supported. 5 | 6 | Libs 7 | ---- 8 | 9 | - Python >= 2.7 or Python >= 3.4 10 | - pySerial >= 3.3 11 | - pyCountry >= 17.0 12 | 13 | Device 14 | ------ 15 | 16 | In order to accept communication of any kind, configure device as follows: 17 | 18 | 1. Press "F" button. 19 | 2. Press 0 - Telium Manager 20 | 3. Press 5 - Init. 21 | 4. Press 1 - Settings 22 | 5. Select - Cashdraw/Checkout connect. 23 | 6. Select "Enable" 24 | 7. Then select your preferred interface (USB, COM1, COM2) 25 | 26 | Finally, reboot your device. 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = pyTeliumManager 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, "3.10.0-rc.1"] 13 | os: [ubuntu-latest] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | pip install -U pip setuptools 24 | pip install -r dev-requirements.txt 25 | - name: Install the package 26 | run: | 27 | python setup.py install 28 | - name: Run tests 29 | run: | 30 | pytest 31 | - uses: codecov/codecov-action@v1 32 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /telium/hexdump.py: -------------------------------------------------------------------------------- 1 | def hexdump(src, length=16, sep='.'): 2 | """ 3 | function that help pretty print bytes content 4 | thank to https://gist.github.com/7h3rAm/5603718 5 | adapted to lib needs. 6 | """ 7 | FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)]) 8 | lines = [] 9 | 10 | for c in range(0, len(src), length): 11 | chars = src[c:c + length] 12 | 13 | hexstr = ' '.join(["%02x" % ord(x) for x in chars]) if isinstance(chars, str) else ' '.join( 14 | ['{:02x}'.format(x) for x in chars]) 15 | 16 | if len(hexstr) > 24: 17 | hexstr = "%s %s" % (hexstr[:24], hexstr[24:]) 18 | 19 | printable = ''.join( 20 | [ 21 | "%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or sep) for x in chars 22 | ] 23 | ) if isinstance(chars, str) else ''.join(['{}'.format((x <= 127 and FILTER[x]) or sep) for x in chars]) 24 | 25 | lines.append("%08x: %-*s |%s|" % (c, length * 3, hexstr, printable)) 26 | print('\n'.join(lines)) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 TAHRI Ahmed R. 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 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. exception:: SignalDoesNotExistException 5 | 6 | Trying to send a unknown signal to device. 7 | 8 | .. exception:: DataFormatUnsupportedException 9 | 10 | Exception raised when trying to send something other than a string sequence to device. 11 | 12 | .. exception:: TerminalInitializationFailedException 13 | 14 | Exception raised when your device doesn't respond with 'ACK' signal when receiving 'ENQ' signal. 15 | Could mean that the device is busy or not well configured. 16 | 17 | .. exception:: TerminalUnrecognizedConstantException 18 | 19 | Exception raised when you've built a TeliumAsk instance without proposed constant from package. 20 | 21 | .. exception:: LrcChecksumException 22 | 23 | Exception raised when your raw bytes sequence does not match computed LRC with actual one from the sequence. 24 | Could mean that your serial/usb conn isn't stable. 25 | 26 | .. exception:: SequenceDoesNotMatchLengthException 27 | 28 | Exception raised when trying to translate object via encode() or decode() doesn't match required output length. 29 | Could mean that your device is currently unsupported. 30 | 31 | .. exception:: IllegalAmountException 32 | 33 | Exception raised when asking for an amount is bellow TERMINAL_MINIMAL_AMOUNT_REQUESTABLE and higher than TERMINAL_MAXIMAL_AMOUNT_REQUESTABLE. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pyTeliumManager documentation master file, created by 2 | sphinx-quickstart on Fri Jun 16 04:30:35 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | =============== 7 | pyTeliumManager 8 | =============== 9 | 10 | Overview 11 | ======== 12 | 13 | This module allow you to manipulate your Ingenico payment device such as IWL250, iCT250 for instance. 14 | Accept USB Emulated Serial Device or Native RS-232 Serial Link. 15 | 16 | .. image:: http://www.vente-terminal-de-paiement.com/wp-content/uploads/2015/07/iwl250-inthehand-fr.jpg 17 | :height: 400px 18 | :width: 400px 19 | :scale: 50 % 20 | :alt: Ingenico iWL250 Mobile Payment Device 21 | :align: right 22 | 23 | 24 | It is released under MIT license, see LICENSE for more 25 | details. Be aware that no warranty of any kind is provided with this package. 26 | 27 | Copyright (C) 2017 Ahmed TAHRI 28 | 29 | Features 30 | ======== 31 | 32 | - Ask for payment in any currency. 33 | - Verify transaction afterward and extract payment source data if needed. 34 | 35 | Contents: 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | 40 | requirements 41 | installation 42 | classes 43 | exceptions 44 | constants 45 | examples 46 | 47 | Indices and tables 48 | ================== 49 | 50 | * :ref:`genindex` 51 | * :ref:`modindex` 52 | * :ref:`search` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # PyCharm 10 | .idea/* 11 | 12 | # local test script 13 | lanceToi.py 14 | 15 | # Distribution / packaging 16 | .Python 17 | .pypirc 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *,cover 53 | .hypothesis/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # IPython Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv/ 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject -------------------------------------------------------------------------------- /docs/constants.rst: -------------------------------------------------------------------------------- 1 | Constants 2 | ========= 3 | 4 | *Answer flag* 5 | 6 | Fullsized report contains payment unique identifier like credit-card numbers, smallsized does not. 7 | 8 | .. data:: TERMINAL_ANSWER_SET_FULLSIZED 9 | .. data:: TERMINAL_ANSWER_SET_SMALLSIZED 10 | 11 | *Transaction type* 12 | 13 | .. data:: TERMINAL_MODE_PAYMENT_DEBIT 14 | .. data:: TERMINAL_MODE_PAYMENT_CREDIT 15 | .. data:: TERMINAL_MODE_PAYMENT_REFUND 16 | .. data:: TERMINAL_MODE_PAYMENT_AUTO 17 | 18 | *Payment mode* 19 | 20 | .. data:: TERMINAL_TYPE_PAYMENT_CARD 21 | .. data:: TERMINAL_TYPE_PAYMENT_CHECK 22 | .. data:: TERMINAL_TYPE_PAYMENT_AMEX 23 | .. data:: TERMINAL_TYPE_PAYMENT_CETELEM 24 | .. data:: TERMINAL_TYPE_PAYMENT_COFINOGA 25 | .. data:: TERMINAL_TYPE_PAYMENT_DINERS 26 | .. data:: TERMINAL_TYPE_PAYMENT_FRANFINANCE 27 | .. data:: TERMINAL_TYPE_PAYMENT_JCB 28 | .. data:: TERMINAL_TYPE_PAYMENT_ACCORD_FINANCE 29 | .. data:: TERMINAL_TYPE_PAYMENT_MONEO 30 | .. data:: TERMINAL_TYPE_PAYMENT_CUP 31 | .. data:: TERMINAL_TYPE_PAYMENT_FINTRAX_EMV 32 | .. data:: TERMINAL_TYPE_PAYMENT_OTHER 33 | 34 | *Delay* 35 | 36 | Instant answer won't contain a valid transaction status. 37 | 38 | .. data:: TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION 39 | .. data:: TERMINAL_REQUEST_ANSWER_INSTANT 40 | 41 | *Authorization* 42 | 43 | Forced authorization control isn't recommended because it could be significantly slower. 44 | You might have some ext. fees when using GPRS based payment device. 45 | 46 | .. data:: TERMINAL_FORCE_AUTHORIZATION_ENABLE 47 | .. data:: TERMINAL_FORCE_AUTHORIZATION_DISABLE 48 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | Most basic usage 5 | ---------------- 6 | 7 | Example of usage:: 8 | 9 | # Open device 10 | my_device = Telium('/dev/ttyACM0') 11 | 12 | # Construct our payment infos 13 | my_payment = TeliumAsk( 14 | '1', # Checkout ID 1 15 | TERMINAL_ANSWER_SET_FULLSIZED, # Ask for fullsized report 16 | TERMINAL_MODE_PAYMENT_DEBIT, # Ask for debit 17 | TERMINAL_TYPE_PAYMENT_CARD, # Using a card 18 | TERMINAL_NUMERIC_CURRENCY_EUR, # Set currency to EUR 19 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, # Wait for transaction to end before getting final answer 20 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, # Let device choose if we should ask for authorization 21 | 12.5 # Ask for 12.5 EUR 22 | ) 23 | 24 | # Send payment infos to device 25 | my_device.ask(my_payment) 26 | 27 | # Wait for terminal to answer 28 | my_answer = my_device.verify(my_payment) 29 | 30 | if my_answer is not None: 31 | # Print answered data from terminal 32 | print(my_answer.__dict__) 33 | 34 | Create TeliumAsk instance from static method 35 | -------------------------------------------- 36 | 37 | Create instance:: 38 | 39 | my_payment = TeliumAsk.new_payment( 40 | 12.5, # Amount you want 41 | payment_mode='debit', # other mode: credit or refund. 42 | target_currency='EUR', 43 | wait_for_transaction_to_end=True, # If you need valid transaction status 44 | collect_payment_source_info=True, # If you need to identify payment source 45 | force_bank_verification=False # Set it to True if you absolutly need more guarantee in this transaction. Could result in slower authorization from bank. 46 | ) 47 | 48 | Use Ingenico payment device thought not emulated serial link 49 | ------------------------------------------------------------ 50 | 51 | .. image:: https://pmcdn.priceminister.com/photo/ingenico-sagem-cable-liaison-1m-vers-pc-ou-caisse-rs232-femelle-et-rj11-1033614629_ML.jpg 52 | :height: 200px 53 | :width: 300px 54 | :scale: 50 % 55 | :alt: Ingenico RS 232 Cable 56 | :align: left 57 | 58 | Init:: 59 | 60 | # It's as easy as this 61 | my_device = TeliumNativeSerial('/dev/ttyS4') 62 | -------------------------------------------------------------------------------- /telium/constant.py: -------------------------------------------------------------------------------- 1 | DELAY_TERMINAL_ANSWER_TRANSACTION = 120 2 | 3 | TERMINAL_DATA_ENCODING = 'ASCII' 4 | 5 | TERMINAL_ANSWER_SET_FULLSIZED = '1' 6 | TERMINAL_ANSWER_SET_SMALLSIZED = '0' 7 | 8 | TERMINAL_FORCE_AUTHORIZATION_ENABLE = 'B011' 9 | TERMINAL_FORCE_AUTHORIZATION_DISABLE = 'B010' 10 | 11 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION = 'A010' 12 | TERMINAL_REQUEST_ANSWER_INSTANT = 'A011' 13 | 14 | TERMINAL_TYPE_PAYMENT_CARD = '1' 15 | TERMINAL_TYPE_PAYMENT_CHECK = 'C' 16 | TERMINAL_TYPE_PAYMENT_AMEX = '2' 17 | TERMINAL_TYPE_PAYMENT_CETELEM = '3' 18 | TERMINAL_TYPE_PAYMENT_COFINOGA = '5' 19 | TERMINAL_TYPE_PAYMENT_DINERS = '6' 20 | TERMINAL_TYPE_PAYMENT_FRANFINANCE = '8' 21 | TERMINAL_TYPE_PAYMENT_JCB = '9' 22 | TERMINAL_TYPE_PAYMENT_ACCORD_FINANCE = 'A' 23 | TERMINAL_TYPE_PAYMENT_MONEO = 'O' 24 | TERMINAL_TYPE_PAYMENT_CUP = 'U' 25 | TERMINAL_TYPE_PAYMENT_FINTRAX_EMV = 'F' 26 | TERMINAL_TYPE_PAYMENT_OTHER = '0' 27 | 28 | TERMINAL_MODE_PAYMENT_DEBIT = '0' 29 | TERMINAL_MODE_PAYMENT_CREDIT = '1' 30 | TERMINAL_MODE_PAYMENT_REFUND = '2' 31 | TERMINAL_MODE_PAYMENT_AUTO = '4' 32 | 33 | TERMINAL_PAYMENT_SUCCESS = 0 34 | TERMINAL_PAYMENT_REJECTED = 7 35 | TERMINAL_PAYMENT_NOT_VERIFIED = 9 36 | 37 | TERMINAL_NUMERIC_CURRENCY_EUR = '978' 38 | TERMINAL_NUMERIC_CURRENCY_USD = '840' 39 | 40 | TERMINAL_ANSWER_COMPLETE_SIZE = 83 # STX + 2 char + 1 char + 8 char + 1 char + 55 char + 3 char + 10 char + ETX + LRC 41 | TERMINAL_ANSWER_LIMITED_SIZE = 28 # STX + 2 char + 1 char + 8 char + 1 char + 3 char + 10 char + ETX + LRC 42 | 43 | TERMINAL_ASK_REQUIRED_SIZE = 34 44 | TERMINAL_MAXIMAL_AMOUNT_REQUESTABLE = 99999.99 45 | TERMINAL_MINIMAL_AMOUNT_REQUESTABLE = 1.00 46 | TERMINAL_DECIMALS_ALLOWED = 2 47 | 48 | TERMINAL_TRANSACTION_TYPES = { 49 | 'debit': TERMINAL_MODE_PAYMENT_DEBIT, 50 | 'credit': TERMINAL_MODE_PAYMENT_CREDIT, 51 | 'refund': TERMINAL_MODE_PAYMENT_REFUND 52 | } 53 | 54 | TERMINAL_PROBABLES_PATH = ['/dev/ttyACM0', '/dev/tty.usbmodem1411'] 55 | 56 | # Borrowed from curses.ascii, should permit to use it on Windows 57 | CONTROL_NAMES = [ 58 | "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", 59 | "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", 60 | "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", 61 | "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", 62 | "SP" 63 | ] 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import io 3 | import os 4 | 5 | from re import search 6 | 7 | 8 | def get_version(): 9 | with open('telium/version.py') as version_file: 10 | return search(r"""__version__\s+=\s+(['"])(?P.+?)\1""", 11 | version_file.read()).group('version') 12 | 13 | 14 | here = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | DESCRIPTION = ('A cross-platform point of sales payment manager tool with Telium Manager ' 17 | 'Support every device with Telium Manager like Ingenico terminals.') 18 | 19 | try: 20 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 21 | long_description = '\n' + f.read() 22 | except FileNotFoundError: 23 | long_description = DESCRIPTION 24 | 25 | setup( 26 | name='pyTeliumManager', 27 | version=get_version(), 28 | author='Ahmed TAHRI, @Ousret', 29 | author_email='ahmed.tahri@cloudnursery.dev', 30 | description=DESCRIPTION, 31 | long_description=long_description, 32 | long_description_content_type='text/markdown', 33 | license='MIT', 34 | packages=['telium'], 35 | test_suite='test', 36 | url='https://github.com/Ousret/pyTeliumManager', 37 | install_requires=[ 38 | 'pyserial>=3.3', 39 | 'pycountry>=17.0,<18.5.20', 40 | 'payment_card_identifier>=0.1.2', 41 | 'six' 42 | ], 43 | tests_require=['Faker', 'pytest'], 44 | keywords=['ingenico', 'telium manager', 'telium', 'payment', 'credit card', 'debit card', 'visa', 'mastercard', 45 | 'merchant', 'pos'], 46 | classifiers=[ 47 | 'Development Status :: 5 - Production/Stable', 48 | 'Environment :: Win32 (MS Windows)', 49 | 'Environment :: X11 Applications', 50 | 'Environment :: MacOS X', 51 | 'Intended Audience :: Developers', 52 | 'License :: OSI Approved :: MIT License', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.1', 58 | 'Programming Language :: Python :: 3.2', 59 | 'Programming Language :: Python :: 3.3', 60 | 'Programming Language :: Python :: 3.4', 61 | 'Programming Language :: Python :: 3.5', 62 | 'Programming Language :: Python :: 3.6', 63 | 'Programming Language :: Python :: 3.7', 64 | 'Programming Language :: Python :: 3.8', 65 | 'Programming Language :: Python :: 3.9', 66 | 'Programming Language :: Python :: 3.10', 67 | ] 68 | ) 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ahmed@tahri.space. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to Ingenico for Human 👋

2 | 3 |

4 | One of the few library that help you use this kind of hardware
5 | 6 | 7 | 8 | 9 | 10 | Download Count /Month 11 | 12 | 13 | License: MIT 14 | 15 | 16 | Code Quality Badge 17 | 18 | 19 | 20 | 21 | 22 | Documentation Status 23 | 24 | Download Count Total 25 |
26 | 27 |

28 | 29 | > Python library to manipulate Ingenico mobile payment device equipped with Telium Manager. RS232/USB. 30 | > Please note that every payment device with Telium Manager should, in theory, work with this. 31 | 32 | ##### PyPi 33 | 34 | *Python 2.7 support has been added to master branch since v2.3.0* 35 | 36 | ```sh 37 | pip install pyTeliumManager --upgrade 38 | ``` 39 | 40 | ##### How to start using pyTeliumManager 41 | 42 | ```python 43 | # Import telium package 44 | from telium import * 45 | 46 | # Open device 47 | my_device = Telium('/dev/ttyACM0') 48 | 49 | # Construct our payment infos 50 | my_payment = TeliumAsk.new_payment( 51 | 12.5, 52 | payment_mode='debit', # other mode: credit or refund. 53 | target_currency='EUR', 54 | wait_for_transaction_to_end=True, # If you need valid transaction status 55 | collect_payment_source_info=True, # If you need to identify payment source 56 | force_bank_verification=False 57 | ) 58 | 59 | # Send payment infos to device 60 | try: 61 | if not my_device.ask(my_payment): 62 | print('Your device just refused your transaction. Try again.') 63 | exit(1) 64 | except TerminalInitializationFailedException as e: 65 | print(format(e)) 66 | exit(2) 67 | 68 | # Wait for terminal to answer 69 | my_answer = my_device.verify(my_payment) 70 | 71 | if my_answer is not None: 72 | # Convert answered data to dict. 73 | print(my_answer.__dict__) 74 | 75 | # > { 76 | # '_pos_number': '01', 77 | # '_payment_mode': '1', 78 | # '_currency_numeric': '978', 79 | # '_amount': 12.5, 80 | # '_private': '0000000000', 81 | # 'has_succeeded': True, 82 | # 'transaction_id': '0000000000', 83 | # '_transaction_result': 0, 84 | # '_repport': '4711690463168807000000000000000000000000000000000000000', 85 | # '_card_type': 86 | # { 87 | # '_name': 'VISA', 88 | # '_regex': '^4[0-9]{12}(?:[0-9]{3})?$', 89 | # '_numbers': '4711690463168807', 90 | # '_masked_numbers': 'XXXXXXXXXXXX8807' 91 | # } 92 | # } 93 | 94 | if my_answer.has_succeeded: 95 | print("Your payment has been processed using a {0} card. Id: {1}".format(my_answer.card_type.name, my_answer.card_type.numbers)) 96 | else: 97 | print("Your payment was rejected. Try again if you wish to.") 98 | ``` 99 | 100 | ##### **How to enable computer liaison with Ingenico device** 101 | 102 | 1. Press "F" button 103 | 2. Press 0 - Telium Manager 104 | 3. Press 5 - Init 105 | 4. Press 1 - Param 106 | 5. Select - Checkout 107 | 6. Select "Enable" 108 | 7. Choose your preferred interface (USB, COM1, COM2) 109 | 110 | **Tested devices:** 111 | 112 | - Ingenico iWL250 113 | - Ingenico iCT220 114 | - Ingenico iCT250 115 | 116 | Should work with all i**2XX device equipped with Telium Manager app. 117 | Feel free to repport issue if your device isn't compatible with this package. 118 | 119 | **Won't work** 120 | 121 | - All direct PinPad liaison, also known as iPP3XX. (see issue #2) 122 | 123 | #### Q-A 124 | 125 | > Will this package cause loss of money in any way ? 126 | - You shouldn't worry about that, I've deployed it for a different store in 2015. No loss has been reported yet. 127 | - If you hesitate on how to use this package, feel free to ask me before using it. 128 | 129 | > My device isn't working with this package. 130 | - Make sure you've followed **How to enable computer liaison with Ingenico device** steps above beforehand. 131 | - If you're on Windows, make sure you've installed the correct driver. 132 | - Try every COM port, one by one. 133 | - On Linux it should be located at */dev/ttyACM0*, if not run ```ls -l /dev/tty* | grep ACM``` to locate it. 134 | 135 | #### Contributions 136 | 137 | Feel free to propose pull requests. This project may be improved in many ways. 138 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyTeliumManager documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 16 04:30:35 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | from recommonmark.parser import CommonMarkParser 24 | import sphinx_rtd_theme 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | # source_suffix = '.rst' 45 | 46 | source_parsers = { 47 | '.md': CommonMarkParser, 48 | } 49 | 50 | source_suffix = ['.rst', '.md'] 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'pyTeliumManager' 57 | copyright = '2017, Ahmed TAHRI' 58 | author = 'Ahmed TAHRI' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = '2.3.0' 66 | # The full version, including alpha/beta/rc tags. 67 | release = '2.3.0' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = False 86 | 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = 'sphinx_rtd_theme' 94 | 95 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ['_static'] 107 | 108 | 109 | # -- Options for HTMLHelp output ------------------------------------------ 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'pyTeliumManagerdoc' 113 | 114 | 115 | # -- Options for LaTeX output --------------------------------------------- 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'pyTeliumManager.tex', 'pyTeliumManager Documentation', 140 | 'Ahmed TAHRI', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output --------------------------------------- 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'pyteliummanager', 'pyTeliumManager Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'pyTeliumManager', 'pyTeliumManager Documentation', 161 | author, 'pyTeliumManager', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /test/test_payment.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main 2 | from telium.payment import TeliumData 3 | from telium import * 4 | 5 | 6 | class TestTPE(TestCase): 7 | 8 | def test_telium_data_not_implemented(self): 9 | 10 | with self.assertRaises(NotImplementedError): 11 | TeliumData.decode(b'') 12 | 13 | def test_telium_ask_size_not_match(self): 14 | 15 | my_payment = TeliumAsk( 16 | '1', 17 | TERMINAL_ANSWER_SET_FULLSIZED, 18 | TERMINAL_MODE_PAYMENT_DEBIT, 19 | TERMINAL_TYPE_PAYMENT_CARD, 20 | TERMINAL_NUMERIC_CURRENCY_USD, 21 | 'NOT USING SPECIFIED CONSTANT', 22 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, 23 | 666.66 24 | ) 25 | 26 | with self.assertRaises(SequenceDoesNotMatchLengthException): 27 | my_payment.encode() 28 | 29 | my_answer = TeliumResponse( 30 | '1', 31 | TERMINAL_PAYMENT_SUCCESS, 32 | 10.91, 33 | 'NOT USING SPECIFIED CONSTANT', 34 | None, 35 | TERMINAL_NUMERIC_CURRENCY_USD, 36 | 10 * '0' 37 | ) 38 | 39 | with self.assertRaises(SequenceDoesNotMatchLengthException): 40 | my_answer.encode() 41 | 42 | raw_invalid_sequence = '0000000000' 43 | 44 | with self.assertRaises(SequenceDoesNotMatchLengthException): 45 | 46 | TeliumAsk.decode( 47 | bytes(raw_invalid_sequence + chr(TeliumAsk.lrc(raw_invalid_sequence[1:])), TERMINAL_DATA_ENCODING) if six.PY3 else bytes(raw_invalid_sequence + chr(TeliumAsk.lrc(raw_invalid_sequence[1:]))) 48 | ) 49 | 50 | with self.assertRaises(SequenceDoesNotMatchLengthException): 51 | TeliumResponse.decode( 52 | bytes(raw_invalid_sequence + chr(TeliumAsk.lrc(raw_invalid_sequence[1:])), TERMINAL_DATA_ENCODING) if six.PY3 else bytes(raw_invalid_sequence + chr(TeliumAsk.lrc(raw_invalid_sequence[1:]))) 53 | ) 54 | 55 | def test_telium_ask_currencies_setter(self): 56 | 57 | my_payment = TeliumAsk( 58 | '1', 59 | TERMINAL_ANSWER_SET_FULLSIZED, 60 | TERMINAL_MODE_PAYMENT_DEBIT, 61 | TERMINAL_TYPE_PAYMENT_CARD, 62 | TERMINAL_NUMERIC_CURRENCY_USD, 63 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, 64 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, 65 | 666.66 66 | ) 67 | 68 | my_payment.currency_numeric = 'EUR' 69 | 70 | self.assertEqual(my_payment.currency_numeric, TERMINAL_NUMERIC_CURRENCY_EUR) 71 | 72 | with self.assertRaises(KeyError): 73 | my_payment.currency_numeric = 'USSSD' 74 | 75 | with self.assertRaises(KeyError): 76 | my_payment.currency_numeric = 'EURO' 77 | 78 | def test_telium_data_decode(self): 79 | 80 | with self.assertRaises(LrcChecksumException): 81 | TeliumResponse.decode(b'ShouldNotBeDecoded') 82 | with self.assertRaises(LrcChecksumException): 83 | TeliumAsk.decode(b'ShouldNotBeDecoded') 84 | 85 | def test_telium_ask_proto_e_len(self): 86 | 87 | my_payment = TeliumAsk( 88 | '1', 89 | TERMINAL_ANSWER_SET_FULLSIZED, 90 | TERMINAL_MODE_PAYMENT_DEBIT, 91 | TERMINAL_TYPE_PAYMENT_CARD, 92 | TERMINAL_NUMERIC_CURRENCY_EUR, 93 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, 94 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, 95 | 55.1 96 | ) 97 | 98 | my_payment_proto_e = my_payment.encode() 99 | 100 | self.assertEqual(len(my_payment_proto_e), 37, 'TeliumAsk encoded ProtoE should be 37 octets long.') 101 | self.assertEqual(len(my_payment_proto_e[1:-2]), 34, 'TeliumAsk encoded ProtoE should be 34 octets long without STX..LRC..ETX') 102 | 103 | def test_telium_response_proto_e_len(self): 104 | my_answer = TeliumResponse( 105 | '1', 106 | TERMINAL_PAYMENT_SUCCESS, 107 | 12.5, 108 | TERMINAL_MODE_PAYMENT_DEBIT, 109 | '0' * 55, 110 | TERMINAL_NUMERIC_CURRENCY_EUR, 111 | '0' * 10 112 | ) 113 | 114 | self.assertEqual(len(my_answer.payment_mode), 1) 115 | self.assertEqual(len(my_answer.pos_number), 2) 116 | self.assertEqual(len(my_answer.private), 10) 117 | self.assertEqual(len(my_answer.currency_numeric), 3) 118 | 119 | def test_telium_answer_proto_decode(self): 120 | 121 | my_answer = TeliumResponse( 122 | '1', 123 | TERMINAL_PAYMENT_SUCCESS, 124 | 12.5, 125 | TERMINAL_MODE_PAYMENT_DEBIT, 126 | '0' * 55, 127 | TERMINAL_NUMERIC_CURRENCY_EUR, 128 | '0' * 10 129 | ) 130 | 131 | print(my_answer.json) 132 | 133 | my_answer_proto_e = my_answer.encode() 134 | 135 | my_answer_restored = TeliumResponse.decode( 136 | bytes(my_answer_proto_e, TERMINAL_DATA_ENCODING) if six.PY3 else bytes(my_answer_proto_e) 137 | ) 138 | 139 | self.assertEqual(my_answer_restored.pos_number, my_answer.pos_number) 140 | self.assertEqual(my_answer_restored.transaction_result, my_answer.transaction_result) 141 | self.assertEqual(my_answer_restored.repport, my_answer.repport) 142 | self.assertEqual(my_answer_restored.currency_numeric, my_answer.currency_numeric) 143 | self.assertEqual(my_answer_restored.payment_mode, my_answer.payment_mode) 144 | self.assertEqual(my_answer_restored.amount, my_answer.amount) 145 | self.assertEqual(my_answer_restored.private, my_answer.private) 146 | self.assertEqual(my_answer.card_id, '0'*55) 147 | self.assertEqual(my_answer.has_succeeded, True) 148 | self.assertEqual(my_answer.transaction_id, '0'*10) 149 | 150 | def test_telium_ask_proto_decode(self): 151 | my_payment = TeliumAsk( 152 | '1', 153 | TERMINAL_ANSWER_SET_FULLSIZED, 154 | TERMINAL_MODE_PAYMENT_DEBIT, 155 | TERMINAL_TYPE_PAYMENT_CARD, 156 | TERMINAL_NUMERIC_CURRENCY_EUR, 157 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, 158 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, 159 | 55.1 160 | ) 161 | 162 | my_payment_proto_e = my_payment.encode() 163 | 164 | my_payment_restored = TeliumAsk.decode( 165 | bytes(my_payment_proto_e, TERMINAL_DATA_ENCODING) if six.PY3 else bytes(my_payment_proto_e) 166 | ) 167 | 168 | self.assertEqual(my_payment_restored.pos_number, my_payment.pos_number, 'pos_number is not equal from original to decoded') 169 | self.assertEqual(my_payment_restored.private, my_payment.private, 'private is not equal from original to decoded') 170 | self.assertEqual(my_payment_restored.answer_flag, my_payment.answer_flag, 'answer_flag is not equal from original to decoded') 171 | self.assertEqual(my_payment_restored.payment_mode, my_payment.payment_mode, 'payment_mode is not equal from original to decoded') 172 | self.assertEqual(my_payment_restored.amount, my_payment.amount, 'amount is not equal from original to decoded') 173 | self.assertEqual(my_payment_restored.delay, my_payment.delay, 'delay is not equal from original to decoded') 174 | self.assertEqual(my_payment_restored.currency_numeric, my_payment.currency_numeric, 'currency_numeric is not equal from original to decoded') 175 | 176 | 177 | if __name__ == '__main__': 178 | main() -------------------------------------------------------------------------------- /test/test_tpe.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main 2 | 3 | import os, pty 4 | import curses.ascii 5 | import threading 6 | 7 | from faker import Faker 8 | from payment_card_identifier import VISA 9 | from telium.payment import TeliumData 10 | from telium import * 11 | 12 | 13 | class FakeTeliumDevice: 14 | 15 | def __init__(self): 16 | self._master, self._slave = pty.openpty() 17 | self._s_name = os.ttyname(self._slave) 18 | 19 | self._fake = Faker() 20 | 21 | self._fake_device = threading.Thread(target=self.__run) 22 | 23 | def run_instance(self): 24 | self._fake_device.start() 25 | 26 | @property 27 | def s_name(self): 28 | return self._s_name 29 | 30 | @staticmethod 31 | def _has_signal(data, signal): 32 | return (data[0] if six.PY3 else ord(data[0])) == curses.ascii.controlnames.index(signal) 33 | 34 | @staticmethod 35 | def _create_signal(signal): 36 | return bytes([curses.ascii.controlnames.index(signal)]) if six.PY3 else bytes(chr(curses.ascii.controlnames.index(signal))) 37 | 38 | def _wait_signal(self, signal): 39 | return FakeTeliumDevice._has_signal(os.read(self._master, 1), signal) 40 | 41 | def _send_signal(self, signal): 42 | os.write(self._master, FakeTeliumDevice._create_signal(signal)) 43 | 44 | def __run(self): 45 | 46 | if self._wait_signal('ENQ'): 47 | 48 | self._send_signal('ACK') 49 | 50 | raw_data = os.read(self._master, TERMINAL_ANSWER_COMPLETE_SIZE) 51 | 52 | if TeliumData.lrc_check(raw_data) is True: 53 | 54 | payment_pending = TeliumAsk.decode(raw_data) 55 | 56 | print('from slave : ', payment_pending.__dict__) 57 | 58 | self._send_signal('ACK') # Accept data from master 59 | 60 | if not self._wait_signal('EOT'): 61 | self._send_signal('NAK') 62 | exit(1) 63 | 64 | if payment_pending.answer_flag == TERMINAL_ANSWER_SET_FULLSIZED: 65 | my_response = TeliumResponse( 66 | payment_pending.pos_number, 67 | TERMINAL_PAYMENT_SUCCESS, 68 | payment_pending.amount, 69 | payment_pending.payment_mode, 70 | (self._fake.credit_card_number(card_type='visa16') + ' ' + '0' * 37), 71 | payment_pending.currency_numeric, 72 | '0' * 10 73 | ) 74 | elif payment_pending.answer_flag == TERMINAL_ANSWER_SET_SMALLSIZED: 75 | my_response = TeliumResponse( 76 | payment_pending.pos_number, 77 | TERMINAL_PAYMENT_SUCCESS, 78 | payment_pending.amount, 79 | payment_pending.payment_mode, 80 | None, 81 | payment_pending.currency_numeric, 82 | '0' * 10 83 | ) 84 | else: 85 | self._send_signal('NAK') 86 | exit(1) 87 | 88 | self._send_signal('ENQ') 89 | 90 | if self._wait_signal('ACK'): 91 | os.write( 92 | self._master, 93 | bytes(my_response.encode(), TERMINAL_DATA_ENCODING) if six.PY3 else bytes(my_response.encode()) 94 | ) 95 | 96 | if self._wait_signal('ACK'): 97 | self._send_signal('EOT') 98 | exit(0) 99 | 100 | self._send_signal('NAK') 101 | 102 | else: 103 | 104 | self._send_signal('NAK') 105 | exit(1) 106 | 107 | else: 108 | self._send_signal('NAK') 109 | exit(1) 110 | 111 | 112 | class TestTPE(TestCase): 113 | 114 | def setUp(self): 115 | self._fake_device = FakeTeliumDevice() 116 | 117 | def test_demande_paiement_fullsized_repport(self): 118 | 119 | self._fake_device.run_instance() 120 | 121 | my_telium_instance = Telium(self._fake_device.s_name) 122 | 123 | self.assertFalse(my_telium_instance.debugging) 124 | 125 | self.assertTrue(my_telium_instance.is_open) 126 | self.assertEqual(my_telium_instance.timeout, 1) 127 | 128 | self.assertTrue(my_telium_instance.close()) 129 | self.assertTrue(my_telium_instance.open()) 130 | 131 | # Construct our payment infos 132 | my_payment = TeliumAsk( 133 | '1', # Checkout ID 1 134 | TERMINAL_ANSWER_SET_FULLSIZED, # Ask for fullsized repport 135 | TERMINAL_MODE_PAYMENT_DEBIT, # Ask for debit 136 | TERMINAL_TYPE_PAYMENT_CARD, # Using a card 137 | TERMINAL_NUMERIC_CURRENCY_EUR, # Set currency to EUR 138 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, # Do not wait for transaction end for terminal answer 139 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, # Let device choose if we should ask for authorization 140 | 12.5 # Ask for 12.5 EUR 141 | ) 142 | 143 | # Send payment infos to device 144 | self.assertTrue(my_telium_instance.ask(my_payment)) 145 | 146 | my_answer = my_telium_instance.verify(my_payment) 147 | 148 | self.assertIsNotNone(my_answer) 149 | 150 | print('from master : ', my_answer.__dict__) 151 | 152 | self.assertEqual(my_answer.transaction_result, 0) 153 | self.assertEqual(my_answer.currency_numeric, TERMINAL_NUMERIC_CURRENCY_EUR) 154 | self.assertEqual(my_answer.private, '0' * 10) 155 | self.assertIsInstance(my_answer.card_type, VISA) 156 | self.assertEqual(my_answer.card_type.numbers, my_answer.repport.split(' ')[0]) 157 | self.assertIsInstance(my_answer.__dict__.get('_card_type'), dict) 158 | self.assertEqual(my_answer.card_id[0], '4') 159 | 160 | self.assertTrue(my_telium_instance.close()) 161 | self.assertFalse(my_telium_instance.close()) 162 | 163 | def test_demande_paiement_smallsized_repport(self): 164 | 165 | self._fake_device.run_instance() 166 | 167 | my_telium_instance = Telium(self._fake_device.s_name, debugging=True) 168 | 169 | self.assertFalse(my_telium_instance.open()) 170 | 171 | self.assertTrue(my_telium_instance.debugging) 172 | 173 | # Construct our payment infos 174 | my_payment = TeliumAsk( 175 | '1', # Checkout ID 1 176 | TERMINAL_ANSWER_SET_SMALLSIZED, # Ask for fullsized repport 177 | TERMINAL_MODE_PAYMENT_DEBIT, # Ask for debit 178 | TERMINAL_TYPE_PAYMENT_CARD, # Using a card 179 | TERMINAL_NUMERIC_CURRENCY_EUR, # Set currency to EUR 180 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, # Do not wait for transaction end for terminal answer 181 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, # Let device choose if we should ask for authorization 182 | 91.1 # Ask for 12.5 EUR 183 | ) 184 | 185 | # Send payment infos to device 186 | self.assertTrue(my_telium_instance.ask(my_payment, True)) 187 | 188 | my_answer = my_telium_instance.verify(my_payment) 189 | 190 | self.assertIsNotNone(my_answer) 191 | 192 | print('from master : ', my_answer.__dict__) 193 | 194 | self.assertEqual(my_answer.transaction_result, 0) 195 | self.assertEqual(my_answer.currency_numeric, TERMINAL_NUMERIC_CURRENCY_EUR) 196 | self.assertEqual(my_answer.private, '0' * 10) 197 | self.assertEqual(my_answer.repport, '') 198 | 199 | def test_initialization_failed(self): 200 | my_telium_instance = Telium(self._fake_device.s_name) 201 | 202 | # Construct our payment infos 203 | my_payment = TeliumAsk( 204 | '1', # Checkout ID 1 205 | TERMINAL_ANSWER_SET_SMALLSIZED, # Ask for fullsized repport 206 | TERMINAL_MODE_PAYMENT_DEBIT, # Ask for debit 207 | TERMINAL_TYPE_PAYMENT_CARD, # Using a card 208 | TERMINAL_NUMERIC_CURRENCY_EUR, # Set currency to EUR 209 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION, # Do not wait for transaction end for terminal answer 210 | TERMINAL_FORCE_AUTHORIZATION_DISABLE, # Let device choose if we should ask for authorization 211 | 91.1 # Ask for 12.5 EUR 212 | ) 213 | 214 | with self.assertRaises(TerminalInitializationFailedException): 215 | my_telium_instance.ask(my_payment) 216 | 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /docs/classes.rst: -------------------------------------------------------------------------------- 1 | Classes 2 | ======= 3 | 4 | Transaction details 5 | ------------------- 6 | 7 | .. class:: TeliumAsk 8 | 9 | .. method:: __init__(pos_number, answer_flag, transaction_type, payment_mode, currency_numeric, delay, authorization, amount) 10 | 11 | :param str pos_number: 12 | Checkout unique identifier from '01' to '99'. 13 | :param str answer_flag: 14 | Answer report size. use :const:`TERMINAL_ANSWER_SET_FULLSIZED` for complete details or :const:`TERMINAL_ANSWER_SET_SMALLSIZED` 15 | for limited answer report. Limited report does not show payment source id, e.g. credit card numbers. 16 | :param str transaction_type: 17 | If transaction is about CREDIT, DEBIT, etc.. . 18 | Use at least one of listed possible values: 19 | :const:`TERMINAL_MODE_PAYMENT_DEBIT`, 20 | :const:`TERMINAL_MODE_PAYMENT_CREDIT`, 21 | :const:`TERMINAL_MODE_PAYMENT_REFUND`, 22 | :const:`TERMINAL_MODE_PAYMENT_AUTO`. 23 | :param str payment_mode: 24 | Type of payment support. 25 | Use at least one of listed possible values: 26 | :const:`TERMINAL_TYPE_PAYMENT_CARD`, 27 | :const:`TERMINAL_TYPE_PAYMENT_CHECK`, 28 | :const:`TERMINAL_TYPE_PAYMENT_AMEX`, 29 | :const:`TERMINAL_TYPE_PAYMENT_CETELEM`, 30 | :const:`TERMINAL_TYPE_PAYMENT_COFINOGA`, 31 | :const:`TERMINAL_TYPE_PAYMENT_DINERS`, 32 | :const:`TERMINAL_TYPE_PAYMENT_FRANFINANCE`, 33 | :const:`TERMINAL_TYPE_PAYMENT_JCB`, 34 | :const:`TERMINAL_TYPE_PAYMENT_ACCORD_FINANCE`, 35 | :const:`TERMINAL_TYPE_PAYMENT_MONEO`, 36 | :const:`TERMINAL_TYPE_PAYMENT_CUP`, 37 | :const:`TERMINAL_TYPE_PAYMENT_FINTRAX_EMV`, 38 | :const:`TERMINAL_TYPE_PAYMENT_OTHER`. 39 | :param str currency_numeric: 40 | Currency ISO format. 41 | Two ISO currency are available as constant. 42 | :const:`TERMINAL_NUMERIC_CURRENCY_EUR`: EUR - € - ISO;978. 43 | :const:`TERMINAL_NUMERIC_CURRENCY_USD`: USD - $ - ISO;840. 44 | :param str delay: 45 | Describe if answer should be immediate (without valid status) or after transaction. 46 | Use at least one of listed possible values: 47 | :const:`TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION`, 48 | :const:`TERMINAL_REQUEST_ANSWER_INSTANT`. 49 | :param str authorization: 50 | Describe if the terminal has to manually authorize payment. 51 | 52 | Use at least one of listed possible values: 53 | :const:`TERMINAL_FORCE_AUTHORIZATION_ENABLE`, 54 | :const:`TERMINAL_FORCE_AUTHORIZATION_DISABLE`. 55 | :param float amount: 56 | Payment amount, min 0.01, max 99999.99. 57 | 58 | This object is meant to be translated into a bytes sequence and transferred to your terminal. 59 | 60 | .. method:: encode() 61 | 62 | :return: Raw string array with payment information 63 | :rtype: str 64 | :exception SequenceDoesNotMatchLengthException: 65 | Will be raised if the string sequence doesn't match required length. Check your instance params. 66 | 67 | Translate object into a string sequence ready to be sent to device. 68 | 69 | .. staticmethod:: decode(data) 70 | 71 | :param bytes data: Raw bytes sequence to be converted into TeliumAsk instance. 72 | :return: Create a new TeliumAsk. 73 | :rtype: TeliumAsk 74 | :exception LrcChecksumException: 75 | Will be raised if LRC checksum doesn't match. 76 | :exception SequenceDoesNotMatchLengthException: 77 | Will be raised if the string sequence doesn't match required length. 78 | 79 | Create a new instance of TeliumAsk from a bytes sequence previously generated with encode(). 80 | This is no use in a production environment. 81 | 82 | .. staticmethod:: new_payment(amount, payment_mode='debit', target_currency='USD', checkout_unique_id='1', wait_for_transaction_to_end=True, collect_payment_source_info=True, force_bank_verification=False) 83 | 84 | :param float amount: Amount requested 85 | :param str payment_mode: Specify transaction type. (debit, credit or refund) 86 | :param str target_currency: Target currency, must be written in letters. (EUR, USD, etc..) 87 | :param str checkout_unique_id: Unique checkout identifer. 88 | :param bool wait_for_transaction_to_end: Set to True if you need valid transaction status otherwise, set it to False. 89 | :param bool collect_payment_source_info: If you want to retrieve specifics data about payment source identification. 90 | :param bool force_bank_verification: Set it to True if your business need to enforce payment verification. 91 | :return: Ready to use TeliumAsk instance 92 | :rtype: TeliumAsk 93 | 94 | Create new TeliumAsk in order to prepare payment. 95 | Most commonly used. 96 | 97 | Transaction results 98 | ------------------- 99 | 100 | .. class:: TeliumResponse 101 | 102 | .. method:: __init__(pos_number, transaction_result, amount, payment_mode, report, currency_numeric, private) 103 | 104 | :param str pos_number: 105 | Checkout unique identifier from '01' to '99'. 106 | :param int transaction_result: 107 | Transaction result. 108 | :param float amount: 109 | Payment authorized/acquired amount. 110 | :param str payment_mode: 111 | Type of payment support. 112 | :param str report: 113 | Contains payment source unique identifier like credit-card numbers when fullsized report is enabled. 114 | :param str currency_numeric: 115 | Currency ISO format. 116 | :param str private: 117 | If supported by your device, contains transaction unique identifier. 118 | 119 | .. attribute:: has_succeeded 120 | 121 | :getter: True if transaction has been authorized, False otherwise. 122 | :type: bool 123 | 124 | .. attribute:: report 125 | 126 | :getter: Contain data like the card numbers for instance. Should be handled wisely. 127 | :type: str 128 | 129 | .. attribute:: transaction_id 130 | 131 | :getter: If supported by your device, contains transaction unique identifier. 132 | :type: bool 133 | 134 | .. attribute:: card_id 135 | 136 | :getter: Read card numbers if available. 137 | :type: str|None 138 | 139 | .. attribute:: card_id_sha512 140 | 141 | :getter: Return payment source id hash repr (sha512) 142 | :type: str|None 143 | 144 | .. attribute:: card_type 145 | 146 | :getter: Return if available payment card type 147 | :type: payment_card_identifier.PaymentCard|None 148 | 149 | Device management 150 | ----------------- 151 | 152 | .. class:: Telium 153 | 154 | .. method:: __init__(path='/dev/ttyACM0', baudrate=9600, timeout=1, open_on_create=True, debugging=False) 155 | 156 | :param path: 157 | Device path. 158 | 159 | :param int baudrate: 160 | Baud rate such as 9600 or 115200 etc. 161 | Constructor do recommend to set it as 9600. 162 | 163 | :param float timeout: 164 | Set a read timeout value. 165 | 166 | :param bool open_on_create: 167 | Specify if device should be immedialty opened on instance creation. 168 | 169 | :param bool debugging: 170 | Set it to True if you want to diagnose your device. Will print to stdout bunch of useful data. 171 | 172 | The port is immediately opened on object creation if open_on_create toggle is True. 173 | 174 | *path* is the device path: depending on operating system. e.g. 175 | ``/dev/ttyACM0`` on GNU/Linux or ``COM3`` on Windows. Please be aware 176 | that a proper driver is needed on Windows in order to create an emulated serial device. 177 | 178 | Possible values for the parameter *timeout* which controls the behavior 179 | of the device instance: 180 | 181 | - ``timeout = None``: wait forever / until requested number of bytes 182 | are received, not recommended. 183 | - ``timeout = 0``: non-blocking mode, return immediately in any case, 184 | returning zero or more, up to the requested number of bytes, use it only when your computer is really fast unless 185 | you don't care about reliability. 186 | - ``timeout = x``: set timeout to ``x`` seconds (float allowed) 187 | returns immediately when the requested number of bytes are available, 188 | otherwise wait until the timeout expires and return all bytes that 189 | were received until then. 190 | 191 | .. staticmethod:: get() 192 | 193 | :return: Fresh new Telium instance or None 194 | :rtype: Telium|None 195 | 196 | Auto-create a new instance of Telium. The device path will be inferred based on most common location. 197 | This won't be reliable if you have more than one emulated serial device plugged-in. Does not work on NT platform. 198 | 199 | .. method:: ask(telium_ask) 200 | 201 | :param TeliumAsk telium_ask: Payment details 202 | :return: True if device has accepted it, False otherwise. 203 | :rtype: bool 204 | 205 | Initialize payment to terminal 206 | 207 | .. method:: verify(telium_ask) 208 | 209 | :param TeliumAsk telium_ask: Payment details previously used on ask() 210 | :return: Transaction results as TeliumResponse, None if nothing was caught from device. 211 | :rtype: TeliumResponse|None 212 | 213 | Wait for answer and convert it to TeliumResponse. 214 | 215 | .. method:: close() 216 | 217 | :return: True if device was previously opened and now closed. False otherwise. 218 | :rtype: bool 219 | 220 | Close device if currently opened. Recommended practice, don't let Python close it from garbage collector. 221 | 222 | .. attribute:: timeout 223 | 224 | :getter: Current timeout set on read. 225 | :type: float 226 | 227 | 228 | Native serial proxy class 229 | ------------------------- 230 | 231 | *Use this class instead of Telium if you're using native serial conn, see examples.* 232 | 233 | .. class:: TeliumNativeSerial -------------------------------------------------------------------------------- /telium/manager.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | 3 | import six 4 | from telium.hexdump import hexdump 5 | from serial import Serial, EIGHTBITS, PARITY_NONE, STOPBITS_ONE, PARITY_EVEN, SEVENBITS 6 | 7 | from telium.constant import * 8 | from telium.payment import TeliumResponse 9 | 10 | 11 | class SignalDoesNotExistException(KeyError): 12 | pass 13 | 14 | 15 | class DataFormatUnsupportedException(TypeError): 16 | pass 17 | 18 | 19 | class TerminalSerialLinkClosedException(IOError): 20 | pass 21 | 22 | 23 | class TerminalInitializationFailedException(IOError): 24 | pass 25 | 26 | 27 | class TerminalUnrecognizedConstantException(Exception): 28 | pass 29 | 30 | 31 | class TerminalUnexpectedAnswerException(IOError): 32 | pass 33 | 34 | 35 | class Telium: 36 | def __init__(self, 37 | path='/dev/ttyACM0', 38 | baudrate=9600, 39 | bytesize=EIGHTBITS, 40 | parity=PARITY_NONE, 41 | stopbits=STOPBITS_ONE, 42 | timeout=1, 43 | open_on_create=True, 44 | debugging=False): 45 | """ 46 | Create Telium device instance 47 | :param str path: str Path to serial emulated device 48 | :param int baudrate: Set baud rate 49 | :param int timeout: Maximum delai before hanging out. 50 | :param bool open_on_create: Define if device has to be opened on instance creation 51 | :param bool debugging: Enable print device <-> host com trace. (stdout) 52 | """ 53 | self._path = path 54 | self._baud = baudrate 55 | self._debugging = debugging 56 | self._device_timeout = timeout 57 | self._device = None 58 | 59 | self._device = Serial( 60 | self._path if open_on_create else None, 61 | baudrate=self._baud, 62 | bytesize=bytesize, 63 | parity=parity, 64 | stopbits=stopbits, 65 | timeout=timeout 66 | ) 67 | 68 | if not open_on_create: 69 | self._device.setPort(self._path) 70 | 71 | @staticmethod 72 | def get(baudrate=9600, timeout=1, open_on_create=True, debugging=False): 73 | """ 74 | Auto-create a new instance of Telium. The device path will be infered based on most commom location. 75 | This won't be reliable if you have more than one emulated serial device plugged-in. 76 | Won't work either on NT plateform. 77 | :param int baudrate: Baudrate. 78 | :param int timeout: Timeout for byte signal waiting. 79 | :param bool open_on_create: If device should be opened on instance creation. 80 | :param bool debugging: Set it to True if you want to trace comm. between device and host. (stdout) 81 | :return: Fresh new Telium instance or None 82 | :rtype: telium.Telium 83 | """ 84 | for path in TERMINAL_PROBABLES_PATH: 85 | probables = glob('%s*' % ''.join(filter(lambda c: not c.isdigit(), path))) 86 | if len(probables) == 1: 87 | return Telium(probables[0], baudrate, timeout, open_on_create, debugging) 88 | return None 89 | 90 | def __del__(self): 91 | if self._device.is_open: 92 | self._device.close() 93 | 94 | @property 95 | def debugging(self): 96 | return self._debugging 97 | 98 | @property 99 | def timeout(self): 100 | """ 101 | Get current timeout value from pySerial device instance 102 | :return: Current timeout setting from device handled by pySerial 103 | :rtype: float 104 | """ 105 | return self._device.timeout 106 | 107 | @timeout.setter 108 | def timeout(self, new_timeout): 109 | self._device_timeout = new_timeout 110 | self._device.timeout = self._device_timeout 111 | 112 | @property 113 | def is_open(self): 114 | """ 115 | Verify whenever the device is actually opened or not via pySerial main instance. 116 | :return: True if still opened. 117 | :rtype: bool 118 | """ 119 | return self._device.is_open 120 | 121 | def close(self): 122 | """ 123 | Close the device if not already closed 124 | :return: True if device succesfuly closed 125 | :rtype: bool 126 | """ 127 | if self._device.is_open: 128 | self._device.close() 129 | return True 130 | return False 131 | 132 | def open(self): 133 | """ 134 | Open the device if not already opened 135 | :return: True if device succesfuly opened 136 | :rtype: bool 137 | """ 138 | if not self._device.is_open: 139 | self._device.open() 140 | return True 141 | return False 142 | 143 | def _send_signal(self, signal): 144 | """ 145 | Send single signal to device like 'ACK', 'NAK', 'EOT', etc.. . 146 | :param signal: str 147 | :return: True if signal was written to device 148 | :rtype: bool 149 | """ 150 | if signal not in CONTROL_NAMES: 151 | raise SignalDoesNotExistException("The ASCII '%s' code doesn't exist." % signal) 152 | if self._debugging: 153 | print('DEBUG :: try send_signal = ', signal) 154 | return self._send(chr(CONTROL_NAMES.index(signal))) == 1 155 | 156 | def _wait_signal(self, signal): 157 | """ 158 | Read one byte from serial device and compare to expected. 159 | :param signal: str 160 | :return: True if received signal match 161 | :rtype: bool 162 | """ 163 | one_byte_read = self._device.read(1) 164 | expected_char = CONTROL_NAMES.index(signal) 165 | 166 | if self._debugging and len(one_byte_read) == 1: 167 | print('DEBUG :: wait_signal_received = ', CONTROL_NAMES[one_byte_read[0] if six.PY3 else ord(one_byte_read[0])]) 168 | 169 | return one_byte_read == (expected_char.to_bytes(1, byteorder='big') if six.PY3 else chr(expected_char)) 170 | 171 | def _send(self, data): 172 | """ 173 | Send data to terminal 174 | :param str data: string representation to convert and send 175 | :return: Lenght of data actually sent 176 | :rtype: int 177 | """ 178 | if not isinstance(data, str): 179 | raise DataFormatUnsupportedException("Type {0} cannont be send to device. " 180 | "Please use string when calling _send method.".format(str(type(data)))) 181 | return self._device.write(data.encode(TERMINAL_DATA_ENCODING)) 182 | 183 | def _read_answer(self, expected_size=TERMINAL_ANSWER_COMPLETE_SIZE): 184 | """ 185 | Download raw answer and convert it to TeliumResponse 186 | :return: TeliumResponse 187 | :raise: TerminalUnexpectedAnswerException If data cannot be converted into telium.TeliumResponse 188 | :rtype: telium.TeliumResponse 189 | """ 190 | raw_data = self._device.read(size=expected_size) 191 | data_len = len(raw_data) 192 | 193 | if self._debugging: 194 | print('<---------------------------- Chunk from Terminal :: {0} byte(s).'.format(data_len)) 195 | hexdump(raw_data) 196 | print('----------------------------> End of Chunk from Terminal') 197 | 198 | if data_len != expected_size: 199 | raise TerminalUnexpectedAnswerException('Raw read expect size = {0} ' 200 | 'but actual size = {1}.'.format(expected_size, data_len)) 201 | 202 | if raw_data[0] != (CONTROL_NAMES.index('STX') if six.PY3 else chr(CONTROL_NAMES.index('STX'))): 203 | raise TerminalUnexpectedAnswerException( 204 | 'The first byte of the answer from terminal should be STX.. Have %02x and except %02x (STX)' % ( 205 | raw_data[0], CONTROL_NAMES.index('STX'))) 206 | if raw_data[-2] != (CONTROL_NAMES.index('ETX') if six.PY3 else chr(CONTROL_NAMES.index('ETX'))): 207 | raise TerminalUnexpectedAnswerException( 208 | 'The byte before final of the answer from terminal should be ETX') 209 | 210 | return TeliumResponse.decode(raw_data) 211 | 212 | def is_ok(self, raspberry_pi=False): 213 | """ 214 | Should in theory return True if your device is ready to receive order. False otherwise. 215 | I can only recommend you to not call this method every time. 216 | :param bool raspberry_pi: Set it to True if you'r running Raspberry PI 217 | :return: True if device appear to be OK, false otherwise. 218 | :rtype: bool 219 | """ 220 | if raspberry_pi: 221 | self._device.timeout = 0.3 222 | self._device.read(size=1) 223 | self._device.timeout = self._device_timeout 224 | 225 | # Send ENQ and wait for ACK 226 | self._send_signal('ENQ') 227 | 228 | if not self._wait_signal('ACK'): 229 | return False 230 | 231 | return self._send_signal('EOT') 232 | 233 | def ask(self, telium_ask, raspberry_pi=False): 234 | """ 235 | Initialize payment to terminal 236 | :param telium.TeliumAsk telium_ask: Payment info 237 | :param bool raspberry_pi: Set it to True if you'r running Raspberry PI 238 | :return: True if device has accepted to begin a new transaction. 239 | :rtype: bool 240 | """ 241 | 242 | if not self.is_open: 243 | raise TerminalSerialLinkClosedException("Your device isn\'t opened yet.") 244 | 245 | if raspberry_pi: 246 | self._device.timeout = 0.3 247 | self._device.read(size=1) 248 | self._device.timeout = self._device_timeout 249 | 250 | # Send ENQ and wait for ACK 251 | self._send_signal('ENQ') 252 | 253 | if not self._wait_signal('ACK'): 254 | raise TerminalInitializationFailedException( 255 | "Payment terminal isn't ready to accept data from host. " 256 | "Check if terminal is properly configured or not busy.") 257 | 258 | # Send transformed TeliumAsk packet to device 259 | self._send(telium_ask.encode()) 260 | 261 | # Verify if device has received everything 262 | if not self._wait_signal('ACK'): 263 | return False 264 | 265 | # End this communication 266 | self._send_signal('EOT') 267 | 268 | return True 269 | 270 | def verify(self, telium_ask, waiting_timeout=DELAY_TERMINAL_ANSWER_TRANSACTION, raspberry_pi=False): 271 | """ 272 | Wait for answer and convert it for you. 273 | :param telium.TeliumAsk telium_ask: Payment info 274 | :param float waiting_timeout: Custom waiting delay in seconds before giving up on waiting ENQ signal. 275 | :param bool raspberry_pi: Set it to True if you'r running Raspberry PI 276 | :return: TeliumResponse, None or Exception 277 | :rtype: telium.TeliumResponse|None 278 | """ 279 | 280 | if not self.is_open: 281 | raise TerminalSerialLinkClosedException("Your device isn\'t opened yet.") 282 | 283 | answer = None # Initializing null variable. 284 | 285 | # Set high timeout in order to wait for device to answer us. 286 | self._device.timeout = waiting_timeout 287 | 288 | # We wait for terminal to answer us. 289 | if self._wait_signal('ENQ'): 290 | 291 | self._send_signal('ACK') # We're about to say that we're ready to accept data. 292 | 293 | if telium_ask.answer_flag == TERMINAL_ANSWER_SET_FULLSIZED: 294 | answer = self._read_answer(TERMINAL_ANSWER_COMPLETE_SIZE) 295 | elif telium_ask.answer_flag == TERMINAL_ANSWER_SET_SMALLSIZED: 296 | answer = self._read_answer(TERMINAL_ANSWER_LIMITED_SIZE) 297 | else: 298 | raise TerminalUnrecognizedConstantException( 299 | "Cannot determine expected answer size because answer flag is unknown.") 300 | 301 | self._send_signal('ACK') # Notify terminal that we've received it all. 302 | 303 | # The terminal should respond with EOT aka. End of Transmission. 304 | if not self._wait_signal('EOT') and not raspberry_pi: 305 | raise TerminalUnexpectedAnswerException( 306 | "Terminal should have ended the communication with 'EOT'. Something's obviously wrong.") 307 | 308 | # Restore device's timeout 309 | self._device.timeout = self._device_timeout 310 | 311 | return answer 312 | 313 | 314 | class TeliumNativeSerial(Telium): 315 | 316 | def __init__(self, 317 | path, 318 | baudrate=9600, 319 | timeout=1, 320 | open_on_create=True, 321 | debugging=False): 322 | super(TeliumNativeSerial, self).__init__( 323 | path, 324 | baudrate=baudrate, 325 | bytesize=SEVENBITS, 326 | parity=PARITY_EVEN, 327 | stopbits=STOPBITS_ONE, 328 | timeout=timeout, 329 | open_on_create=open_on_create, 330 | debugging=debugging) 331 | -------------------------------------------------------------------------------- /telium/payment.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from abc import ABCMeta, abstractmethod 4 | from functools import reduce 5 | from operator import xor 6 | 7 | import six 8 | from payment_card_identifier import CardIdentifier 9 | from pycountry import currencies 10 | 11 | from telium.constant import * 12 | 13 | 14 | class TeliumDataException(Exception): 15 | pass 16 | 17 | 18 | class LrcChecksumException(TeliumDataException): 19 | pass 20 | 21 | 22 | class SequenceDoesNotMatchLengthException(TeliumDataException): 23 | pass 24 | 25 | 26 | class IllegalAmountException(TeliumDataException): 27 | pass 28 | 29 | 30 | class TeliumData(six.with_metaclass(ABCMeta, object)): 31 | """ 32 | Base class for Telium Manager packet struct. 33 | Shouldn't be used as is. Use TeliumAsk or TeliumResponse. 34 | """ 35 | 36 | def __init__(self, pos_number, amount, payment_mode, currency_numeric, private): 37 | """ 38 | :param str pos_number: Checkout ID, min 1, max 99. 39 | :param float amount: Payment amount, min 1.0, max 99999.99. 40 | :param str payment_mode: Type of payment support, please refers to provided constants. 41 | :param str currency_numeric: Type of currency ISO format, please use specific setter. 42 | :param str private: Terminal reserved. used to store authorization id if any. 43 | """ 44 | self._pos_number = pos_number 45 | self._payment_mode = payment_mode 46 | self._currency_numeric = currency_numeric 47 | self._amount = amount 48 | self._private = private 49 | 50 | try: 51 | int(currency_numeric) 52 | except ValueError: 53 | self.currency_numeric = currency_numeric 54 | 55 | if not TeliumData.is_amount_valid(self.amount): 56 | raise IllegalAmountException( 57 | 'Amount "{0}" is out of bound. Min {1} | Max {2}. {3} Decimals allowed.'.format(self.amount, 58 | TERMINAL_MINIMAL_AMOUNT_REQUESTABLE, 59 | TERMINAL_MAXIMAL_AMOUNT_REQUESTABLE, 60 | TERMINAL_DECIMALS_ALLOWED)) 61 | 62 | @staticmethod 63 | def is_amount_valid(amount): 64 | """ 65 | Check if provided amount is allowed. 66 | :param float amount: Amount 67 | :return: True if provided amount is correct. 68 | :rtype: bool 69 | """ 70 | return isinstance(amount, float) and len(str(amount).split('.')[-1]) <= TERMINAL_DECIMALS_ALLOWED \ 71 | and TERMINAL_MAXIMAL_AMOUNT_REQUESTABLE >= amount >= TERMINAL_MINIMAL_AMOUNT_REQUESTABLE 72 | 73 | @property 74 | def pos_number(self): 75 | """ 76 | Indicate your checkout id 77 | :return: Checkout id 78 | :rtype: str 79 | """ 80 | return self._pos_number.zfill(2) 81 | 82 | @property 83 | def payment_mode(self): 84 | """ 85 | Set default payment mode, DEBIT, CREDIT, REFUND or AUTO. 86 | :return: Payment mode 87 | :rtype: str 88 | """ 89 | return self._payment_mode 90 | 91 | @property 92 | def currency_numeric(self): 93 | return self._currency_numeric 94 | 95 | @currency_numeric.setter 96 | def currency_numeric(self, currency): 97 | currency = currencies.get(alpha_3=currency.upper()) 98 | if currency is None: 99 | raise KeyError('"{cur}" is not available in pyCountry currencies list.'.format(cur=currency)) 100 | self._currency_numeric = str(currency.numeric).zfill(3) 101 | 102 | @property 103 | def private(self): 104 | """ 105 | Unique transaction id if supported by device. 106 | :return: Transaction UUID 107 | :rtype: str 108 | """ 109 | return self._private 110 | 111 | @property 112 | def amount(self): 113 | """ 114 | Payment amount 115 | :return: Amount 116 | :rtype: float 117 | """ 118 | return self._amount 119 | 120 | @staticmethod 121 | def lrc(data): 122 | """ 123 | Calc. LRC from data. Checksum 124 | :param bytes|str data: Data from which LRC checksum should be computed 125 | :return: 0x00 < Result < 0xFF 126 | :rtype: int 127 | """ 128 | if isinstance(data, str): 129 | data = data.encode(TERMINAL_DATA_ENCODING) 130 | elif not isinstance(data, bytes): 131 | raise TypeError("Cannot compute LRC of type {0}. Expect string or bytes.".format(str(type(data)))) 132 | return reduce(xor, [c for c in data]) if six.PY3 else reduce(xor, [ord(c) for c in data]) 133 | 134 | @staticmethod 135 | def lrc_check(data): 136 | """ 137 | Verify if a chunk of data from terminal has a valid LRC checksum. 138 | :param data: raw data from terminal 139 | :return: True if LRC was verified 140 | :rtype: bool 141 | """ 142 | return TeliumData.lrc(data[1:-1]) == (data[-1] if six.PY3 else ord(data[-1])) 143 | 144 | @staticmethod 145 | def framing(packet): 146 | """ 147 | STXETX Framing Encapsulation 148 | :param str packet: RAW string packet 149 | :return: Framed data with ETX..STX.LRC 150 | """ 151 | packet += chr(CONTROL_NAMES.index('ETX')) 152 | return chr(CONTROL_NAMES.index('STX')) + packet + chr(TeliumData.lrc(packet)) 153 | 154 | @abstractmethod 155 | def encode(self): 156 | raise NotImplementedError 157 | 158 | @staticmethod 159 | def decode(data): 160 | """ 161 | Create TeliumData instance from raw bytes data 162 | :param bytes data: raw sequence from terminal 163 | :return: New exploitable instance from raw data 164 | """ 165 | raise NotImplementedError 166 | 167 | @property 168 | def __dict__(self): 169 | return { 170 | '_pos_number': self.pos_number, 171 | '_payment_mode': self.payment_mode, 172 | '_currency_numeric': self.currency_numeric, 173 | '_amount': self.amount, 174 | '_private': self.private 175 | } 176 | 177 | @property 178 | def json(self): 179 | """ 180 | Serialize instance to JSON string 181 | :return: JSON representation-like of instance 182 | :rtype: str 183 | """ 184 | return json.dumps(self, default=lambda o: o.__dict__, 185 | sort_keys=True, indent=4) 186 | 187 | 188 | class TeliumAsk(TeliumData): 189 | def __init__(self, pos_number, answer_flag, transaction_type, payment_mode, currency_numeric, delay, authorization, 190 | amount): 191 | super(TeliumAsk, self).__init__(pos_number, amount, payment_mode, currency_numeric, ' ' * 10) 192 | self._answer_flag = answer_flag 193 | self._transaction_type = transaction_type 194 | self._payment_mode = payment_mode 195 | self._delay = delay 196 | self._authorization = authorization 197 | 198 | @property 199 | def answer_flag(self): 200 | """ 201 | Whenever ask for extended data in answer. 202 | Should correspond to one of the provided constants. 203 | :return: '1' or '0' 204 | :rtype: str 205 | """ 206 | return self._answer_flag 207 | 208 | @property 209 | def transaction_type(self): 210 | return self._transaction_type 211 | 212 | @property 213 | def delay(self): 214 | """ 215 | Describe if answer should be immediate (without valid status) or after transaction. 216 | :return: 'A010' | 'A011' 217 | :rtype: str 218 | """ 219 | return self._delay 220 | 221 | @property 222 | def authorization(self): 223 | """ 224 | Describe if the terminal has to manually authorize payment. 225 | TERMINAL_FORCE_AUTHORIZATION_ENABLE: 'B011' 226 | TERMINAL_FORCE_AUTHORIZATION_DISABLE: 'B010' 227 | :return: 'B011' | 'B010' 228 | :rtype: str 229 | """ 230 | return self._authorization 231 | 232 | def encode(self): 233 | """ 234 | Transform current object so it could be transfered to device (Protocol E) 235 | :return: Str raw array with payment information 236 | :rtype: str 237 | """ 238 | packet = ( 239 | 240 | str(self.pos_number) + # 2 octets 0:3 241 | 242 | ('%.0f' % (self.amount * 100)).zfill(8) + # 8 octets 3:11 243 | 244 | self.answer_flag + # 1 octet 11:12 245 | 246 | self.payment_mode + # 1 octet 12:13 247 | 248 | self.transaction_type + # 1 octet 13:14 249 | 250 | self.currency_numeric + # 3 octet 14:17 251 | 252 | self.private + # 10 octet 17:27 253 | 254 | self.delay + # 4 octet 27:31 255 | 256 | self.authorization) # 4 octet 31:35 257 | 258 | packet_len = len(packet) 259 | 260 | if packet_len != TERMINAL_ASK_REQUIRED_SIZE: 261 | raise SequenceDoesNotMatchLengthException('Cannot create ask payment sequence with len != {0} octets. ' 262 | 'Currently have {1} octet(s).'.format 263 | (TERMINAL_ASK_REQUIRED_SIZE, packet_len)) 264 | 265 | return TeliumData.framing(packet) 266 | 267 | @staticmethod 268 | def decode(data): 269 | """ 270 | Create TeliumAsk from raw str include ETX.....STX.LRC 271 | :param bytes data: Raw bytes sequence. 272 | :return: TeliumAsk 273 | :rtype: telium.TeliumAsk 274 | """ 275 | if TeliumData.lrc_check(data) is False: 276 | raise LrcChecksumException('Cannot decode data with erroned LRC check.') 277 | 278 | raw_message = data[1:-2].decode(TERMINAL_DATA_ENCODING) 279 | 280 | data_len = len(raw_message) 281 | 282 | if data_len != TERMINAL_ASK_REQUIRED_SIZE: 283 | raise SequenceDoesNotMatchLengthException('Cannot decode ask payment sequence with len != {0} octets. ' 284 | 'Currently have {1} octet(s).' 285 | .format(TERMINAL_ASK_REQUIRED_SIZE, data_len)) 286 | 287 | return TeliumAsk( 288 | raw_message[0:2], # pos_number 289 | raw_message[10], # answer_flag 290 | raw_message[12], # transaction_type 291 | raw_message[11], # payment_mode 292 | raw_message[13:16], # currency_numeric 293 | raw_message[26:30], # delay 294 | raw_message[30:34], # authorization 295 | float(raw_message[2:8] + '.' + raw_message[8:10]) # amount 296 | ) 297 | 298 | @property 299 | def __dict__(self): 300 | 301 | new_dict = super(TeliumAsk, self).__dict__ 302 | 303 | new_dict.update({ 304 | '_answer_flag': self.answer_flag, 305 | '_transaction_type': self.transaction_type, 306 | '_payment_mode': self.payment_mode, 307 | '_delay': self.delay, 308 | '_authorization': self.authorization 309 | }) 310 | 311 | return new_dict 312 | 313 | @staticmethod 314 | def new_payment( 315 | amount, 316 | payment_mode='debit', 317 | target_currency='USD', 318 | checkout_unique_id='1', 319 | wait_for_transaction_to_end=True, 320 | collect_payment_source_info=True, 321 | force_bank_verification=False): 322 | """ 323 | Create new TeliumAsk in order to prepare debit payment. 324 | Most commonly used. 325 | :param float amount: Amount requested 326 | :param str payment_mode: Specify transaction type. (debit, credit or refund) 327 | :param str target_currency: Target currency, must be written in letters. (EUR, USD, etc..) 328 | :param str checkout_unique_id: Unique checkout identifer. 329 | :param bool wait_for_transaction_to_end: Set to True if you need valid transaction status otherwise, set it to False. 330 | :param bool collect_payment_source_info: If you want to retrieve specifics data about payment source identification. 331 | :param bool force_bank_verification: Set it to True if your business need to enforce payment verification. 332 | :return: Ready to use TeliumAsk instance 333 | :rtype: TeliumAsk 334 | """ 335 | 336 | if payment_mode.lower() not in TERMINAL_TRANSACTION_TYPES.keys(): 337 | raise TeliumDataException('Unregonized transaction type: "{0}". Allowed: "{1}"'.format(payment_mode, TERMINAL_TRANSACTION_TYPES)) 338 | 339 | return TeliumAsk( 340 | checkout_unique_id, 341 | TERMINAL_ANSWER_SET_FULLSIZED if collect_payment_source_info else TERMINAL_ANSWER_SET_SMALLSIZED, 342 | TERMINAL_TRANSACTION_TYPES[payment_mode.lower()], 343 | TERMINAL_TYPE_PAYMENT_CARD, 344 | target_currency, 345 | TERMINAL_REQUEST_ANSWER_WAIT_FOR_TRANSACTION if wait_for_transaction_to_end else TERMINAL_REQUEST_ANSWER_INSTANT, 346 | TERMINAL_FORCE_AUTHORIZATION_DISABLE if not force_bank_verification else TERMINAL_FORCE_AUTHORIZATION_ENABLE, 347 | amount 348 | ) 349 | 350 | 351 | class TeliumResponse(TeliumData): 352 | def __init__(self, pos_number, transaction_result, amount, payment_mode, repport, currency_numeric, private): 353 | super(TeliumResponse, self).__init__(pos_number, amount, payment_mode, currency_numeric, private) 354 | self._transaction_result = transaction_result 355 | self._repport = repport if repport is not None else '' 356 | self._card_type = CardIdentifier.from_numbers(self._repport.split(' ')[0]) \ 357 | if self._repport not in [None, ''] else None 358 | 359 | @property 360 | def transaction_result(self): 361 | """ 362 | TERMINAL_PAYMENT_SUCCESS: 0 363 | TERMINAL_PAYMENT_REJECTED: 7 364 | TERMINAL_PAYMENT_NOT_VERIFIED: 9 365 | :return: Result provided after transaction. Should'nt be different than 0, 7 or 9. 366 | :rtype: int 367 | """ 368 | return self._transaction_result 369 | 370 | @property 371 | def repport(self): 372 | """ 373 | Contain data like the card numbers for instance. 374 | Should be handled wisely. 375 | :return: RAW Repport 376 | :rtype: str 377 | """ 378 | return self._repport if self._repport is not None else '' 379 | 380 | @property 381 | def has_succeeded(self): 382 | """ 383 | Verify if payment has been succesfuly processed. 384 | :return: True if payment has been approved 385 | :rtype: bool 386 | """ 387 | return self.transaction_result == TERMINAL_PAYMENT_SUCCESS 388 | 389 | @property 390 | def card_type(self): 391 | """ 392 | Return if available payment card type 393 | :return: Card type if available 394 | :rtype: payment_card_identifier.PaymentCard|None 395 | """ 396 | return self._card_type 397 | 398 | @property 399 | def card_id(self): 400 | """ 401 | Read card numbers if available. 402 | Return PaymentCard instance. 403 | :return: Card numbers 404 | :rtype: str 405 | """ 406 | return self._card_type.numbers if self._card_type is not None else self._repport 407 | 408 | @property 409 | def card_id_sha512(self): 410 | """ 411 | Return payment source id hash (sha512) 412 | :return: Hash repr of payment source id. 413 | :rtype: str 414 | """ 415 | return hashlib.sha512(self.card_id.encode('utf-8')).hexdigest() if self._card_type is not None else None 416 | 417 | @property 418 | def transaction_id(self): 419 | """ 420 | Return transaction id generated by device if available. 421 | This method is an alias of self.private 422 | :return: Transaction unique id. 423 | :rtype: str 424 | """ 425 | return self.private 426 | 427 | def encode(self): 428 | """ 429 | Test purpose only. No use in production env. 430 | :return: Str message to be sent to master 431 | :rtype: str 432 | """ 433 | 434 | packet = ( 435 | 436 | str(self.pos_number) + # 2 octets 437 | 438 | str(self.transaction_result) + # 1 octet 439 | 440 | ('%.0f' % (self.amount * 100)).zfill(8) + # 8 octets 441 | 442 | str(self.payment_mode) + # 1 octet 443 | 444 | str(self.repport) + # 55 octets 445 | 446 | str(self.currency_numeric) + # 3 octets 447 | 448 | str(self.private) # 10 octets 449 | 450 | ) 451 | 452 | packet_len = len(packet) 453 | 454 | if packet_len not in [TERMINAL_ANSWER_COMPLETE_SIZE - 3, TERMINAL_ANSWER_LIMITED_SIZE - 3]: 455 | raise SequenceDoesNotMatchLengthException( 456 | 'Cannot create response payment sequence with len != {0} or {1} octet(s) ' 457 | 'Currently have {2} octet(s).' 458 | .format(TERMINAL_ANSWER_COMPLETE_SIZE - 3, TERMINAL_ANSWER_LIMITED_SIZE - 3, packet_len)) 459 | 460 | return TeliumData.framing(packet) 461 | 462 | @staticmethod 463 | def decode(data): 464 | """ 465 | Create TeliumResponse from raw bytes array 466 | :param bytes data: Raw bytes answer from terminal 467 | :return: New instance of TeliumResponse from raw bytes sequence. 468 | :rtype: telium.TeliumResponse 469 | """ 470 | 471 | if TeliumData.lrc_check(data) is False: 472 | raise LrcChecksumException('Cannot decode data with erroned LRC check.') 473 | 474 | raw_message = data[1:-2].decode(TERMINAL_DATA_ENCODING) 475 | data_size = len(data) 476 | 477 | if data_size == TERMINAL_ANSWER_COMPLETE_SIZE: 478 | report, currency_numeric, private = raw_message[12:67], raw_message[67:70], raw_message[70:80] 479 | elif data_size == TERMINAL_ANSWER_LIMITED_SIZE: 480 | report, currency_numeric, private = '', raw_message[12:15], raw_message[15:25] 481 | else: 482 | raise SequenceDoesNotMatchLengthException('Cannot decode raw sequence with length = {0}, ' 483 | 'should be {1} octet(s) or {2} octet(s) long.' 484 | .format(data_size, TERMINAL_ANSWER_COMPLETE_SIZE, 485 | TERMINAL_ANSWER_LIMITED_SIZE)) 486 | 487 | pos_number, transaction_result, amount, payment_mode = raw_message[0:2], int(raw_message[2]), float( 488 | raw_message[3:9] + '.' + raw_message[9:11]), raw_message[11] 489 | 490 | return TeliumResponse( 491 | pos_number, 492 | transaction_result, 493 | amount, 494 | payment_mode, 495 | report, 496 | currency_numeric, 497 | private 498 | ) 499 | 500 | @property 501 | def __dict__(self): 502 | 503 | new_dict = super(TeliumResponse, self).__dict__ # Copying parent __dict__ 504 | 505 | new_dict.update({ # Merge the parent one with this new one 506 | 'has_succeeded': self.has_succeeded, 507 | 'transaction_id': self.transaction_id, 508 | '_transaction_result': self.transaction_result, 509 | '_repport': self.repport, 510 | '_card_type': { 511 | '_name': self.card_type.name, 512 | '_regex': self.card_type.regex.pattern, 513 | '_numbers': self.card_type.numbers, 514 | '_masked_numbers': self.card_type.masked_numbers(), 515 | '_sha512_numbers': self.card_id_sha512 516 | } if self.card_type is not None else None 517 | }) 518 | 519 | return new_dict # Return new dict 520 | --------------------------------------------------------------------------------