├── MANIFEST.in ├── fire ├── __init__.py ├── entities │ ├── __init__.py │ ├── end_of_transmission.py │ ├── end_of_payer.py │ ├── extension_of_time.py │ ├── payer.py │ ├── transmitter.py │ └── payees.py ├── schema │ ├── __init__.py │ ├── 1099_NEC_schema.json │ ├── base_schema.json │ └── 1099_MISC_schema.json └── translator │ ├── __init__.py │ ├── util.py │ └── translator.py ├── bin └── fire-1099 ├── setup.py ├── LICENSE ├── spec ├── data │ ├── valid_transmitter.json │ ├── valid_minimal_MISC.json │ ├── valid_all_MISC.json │ └── valid_standard_MISC.json ├── test_entity_payer.py ├── test_entity_payee.py ├── spec_util.py ├── test_entity_transmitter.py └── test_translator.py ├── .gitignore ├── README.md └── .pylintrc /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include fire/schema/*.json 2 | -------------------------------------------------------------------------------- /fire/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module entrypoint 3 | """ 4 | -------------------------------------------------------------------------------- /fire/entities/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module entrypoint 3 | """ 4 | -------------------------------------------------------------------------------- /fire/schema/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module entrypoint 3 | """ 4 | -------------------------------------------------------------------------------- /fire/translator/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module entrypoint 3 | """ 4 | 5 | from .translator import cli 6 | -------------------------------------------------------------------------------- /bin/fire-1099: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Main entrypoint 5 | """ 6 | 7 | from fire.translator import cli 8 | 9 | cli() 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module packaging 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name='fire-1099', 9 | description='Generate 1099-MISC files for transmission through the \ 10 | IRS FIRE system', 11 | long_description=open('README.md').read(), 12 | 13 | license='MIT', 14 | 15 | author='Stephen Johnson', 16 | author_email='4stephen.j@gmail.com', 17 | url='https://github.com/djeserkare/fire-1099', 18 | 19 | version='0.0.1-alpha', 20 | 21 | packages=find_packages(exclude=['contrib', 'docs', 'tests*', 'spec*']), 22 | include_package_data=True, 23 | install_requires=['click', 'jsonschema'], 24 | scripts=['bin/fire-1099'], 25 | 26 | classifiers=[ 27 | 'Development Status :: 4 - Alpha', 28 | 'Environment :: Console', 29 | 'Intended Audience :: Developers', 30 | 'Natural Language :: English', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7' 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 stephen johnson 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 | -------------------------------------------------------------------------------- /spec/data/valid_transmitter.json: -------------------------------------------------------------------------------- 1 | { 2 | "transmitter":{ 3 | "record_type": "T", 4 | "payment_year": "2017", 5 | "prior_year_data_indicator": "", 6 | "transmitter_tin": "123456789", 7 | "transmitter_control_code": "55AA5", 8 | "test_file_indicator": "T", 9 | "foreign_entity_indicator": "X", 10 | "transmitter_name": "ASDF GLOBAL INC", 11 | "transmitter_name_contd": "", 12 | "company_name": "ASDF GLOBAL INC", 13 | "company_name_contd": "", 14 | "company_mailing_address": "123 ASDF STREET", 15 | "company_city": "NEW YORK", 16 | "company_state": "NY", 17 | "company_zip_code": "10001", 18 | "total_number_of_payees": "2", 19 | "contact_name": "RONALD SWANSON", 20 | "contact_telephone_number_and_ext": "5555555555", 21 | "contact_email_address": "ronald@swanson.com", 22 | "record_sequence_number": "00000001", 23 | "vendor_indicator": "1", 24 | "vendor_name": "GSG Corp", 25 | "vendor_mailing_address": "1234 POIU St", 26 | "vendor_city": "TAXVILLE", 27 | "vendor_state": "TX", 28 | "vendor_zip_code": "10991", 29 | "vendor_contact_name": "BLERD FLERPLERMERD", 30 | "vendor_contact_telephone_and_ext": "5557776666", 31 | "vendor_foreign_entity_indicator": "1" 32 | } 33 | } -------------------------------------------------------------------------------- /spec/data/valid_minimal_MISC.json: -------------------------------------------------------------------------------- 1 | { 2 | "transmitter":{ 3 | "transmitter_name":"ASDF GLOBAL INC", 4 | "company_name":"ASDF GLOBAL INC", 5 | "company_mailing_address":"123 ASDF STREET", 6 | "company_city":"NEW YORK", 7 | "company_state":"NY", 8 | "company_zip_code":"10001", 9 | "transmitter_tin":"123456789", 10 | "test_file_indicator":"T", 11 | "transmitter_control_code":"55AA5", 12 | "contact_name":"RONALD SWANSON", 13 | "contact_telephone_number_and_ext":"5555555555", 14 | "contact_email_address":"ronald@swanson.com", 15 | "payment_year":"2017", 16 | "prior_year_indicator":"" 17 | }, 18 | "payer":{ 19 | "first_payer_name":"ASDF GLOBAL INC", 20 | "payment_year":"2017", 21 | "payer_shipping_address":"123 ASDF STREET", 22 | "payer_city":"NEW YORK", 23 | "payer_state":"NY", 24 | "payer_zip_code":"10001", 25 | "payer_tin":"123456789", 26 | "payer_name_control":"ASDF", 27 | "payer_telephone_number_and_ext":"5555555555" 28 | }, 29 | "payees":[ 30 | { 31 | "first_payee_name_line":"SPACELEY SPROCKETS", 32 | "payees_name_control":"SPAC", 33 | "payment_year":"2017", 34 | "payee_mailing_address":"5678 INDUSTRY PLACE", 35 | "payee_city":"MOON", 36 | "payee_state":"CA", 37 | "payee_zip_code":"22222", 38 | "payees_tin":"987654321", 39 | "phone":"5555555555", 40 | "payment_amount_1":"10000" 41 | }, 42 | { 43 | "first_payee_name_line":"BOB LOBLAW LLP", 44 | "payees_name_control":"BOBL", 45 | "payment_year":"2017", 46 | "payee_mailing_address":"100 LAWBOMB RD", 47 | "payee_city":"BLOBVILLE", 48 | "payee_state":"CA", 49 | "payee_zip_code":"11111", 50 | "payees_tin":"098765432", 51 | "phone":"5556667777", 52 | "payment_amount_1":"5000" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # LOCAL FILES 2 | /data/ 3 | *.sublime-workspace 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /fire/entities/end_of_transmission.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: entities.end_of_transmission 3 | Representation of an "end_of_transmission" record, including transformation 4 | functions and support functions for conversion into different formats. 5 | """ 6 | from fire.translator.util import rjust_zero 7 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 8 | 9 | """ 10 | _END_OF_TRANSMISSION_TRANSFORMS 11 | ----------------------- 12 | Stores metadata associated with each field in a Transmitter record. 13 | Values in key-value pairs represent metadata in the following format: 14 | 15 | (default value, length, fill character, transformation function) 16 | """ 17 | 18 | _ITEMS = [ 19 | ("record_type", ("F", 1, "\x00", lambda x: x)), 20 | ("number_of_a_records", ("00000000", 8, "0", lambda x: rjust_zero(x, 8))), 21 | ("zeros", (21*"0", 21, "0", lambda x: x)), 22 | ("blank_1", ("", 19, "\x00", lambda x: x)), 23 | ("total_number_of_payees", 24 | ("00000000", 8, "0", lambda x: rjust_zero(x, 8))), 25 | ("blank_2", ("", 442, "\x00", lambda x: x)), 26 | ("record_sequence_number", ("", 8, "0", lambda x: x)), 27 | ("blank_3", ("", 241, "\x00", lambda x: x)), 28 | ("blank_4", ("", 2, "\x00", lambda x: x)) 29 | ] 30 | 31 | _END_OF_TRANSMISSION_SORT, _END_OF_TRANSMISSION_TRANSFORMS = \ 32 | factor_transforms(_ITEMS) 33 | 34 | def xform(data): 35 | """ 36 | Applies transformation functions definted in _END_OF_TRANSMISSION_TRANSFORMS 37 | to data supplied as parameter to respective key-value pairs provided as the 38 | input parameter. 39 | 40 | Parameters 41 | ---------- 42 | data : dict 43 | Expects data parameter to have keys that exist in the 44 | _END_OF_TRANSMISSION_TRANSFORMS dict. 45 | 46 | Returns 47 | ---------- 48 | dict 49 | Dictionary containing processed (transformed) data provided as a 50 | parameter. 51 | """ 52 | return xform_entity(_END_OF_TRANSMISSION_TRANSFORMS, data) 53 | 54 | def fire(data): 55 | """ 56 | Returns the given record as a string formatted to the IRS Publication 1220 57 | specification, based on data supplied as parameter. 58 | 59 | Parameters 60 | ---------- 61 | data : dict 62 | Expects data parameter to have all keys specified in 63 | _END_OF_TRANSMISSION_TRANSFORMS. 64 | 65 | Returns 66 | ---------- 67 | str 68 | String formatted to meet IRS Publication 1220 69 | """ 70 | return fire_entity( 71 | _END_OF_TRANSMISSION_TRANSFORMS, 72 | _END_OF_TRANSMISSION_SORT, data) 73 | -------------------------------------------------------------------------------- /fire/entities/end_of_payer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: entities.end_of_payer 3 | Representation of an "end_of_payer" record, including transformation functions 4 | and support functions for conversion into different formats. 5 | """ 6 | from itertools import chain 7 | 8 | from fire.translator.util import rjust_zero 9 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 10 | 11 | """ 12 | _END_OF_PAYER_TRANSFORMS 13 | ----------------------- 14 | Stores metadata associated with each field in a Transmitter record. 15 | Values in key-value pairs represent metadata in the following format: 16 | 17 | (default value, length, fill character, transformation function) 18 | """ 19 | 20 | _ITEMS = [ 21 | ("record_type", ("C", 1, "\x00", lambda x: x)), 22 | ("number_of_payees", ("", 8, "0", lambda x: x)), 23 | ("blank_1", ("", 6, "\x00", lambda x: x)), 24 | ] 25 | 26 | for field in chain((x for x in range(1, 10)), \ 27 | (chr(x) for x in range(ord('A'), ord('I'))), \ 28 | 'J'): 29 | _ITEMS.append((f"payment_amount_{field}", 30 | (18*"0", 18, "0", lambda x: rjust_zero(x, 18)))) 31 | 32 | _ITEMS += [ 33 | ("blank_2", ("", 160, "\x00", lambda x: x)), 34 | ("record_sequence_number", ("", 8, "0", lambda x: x)), 35 | ("blank_3", ("", 241, "\x00", lambda x: x)), 36 | ("blank_4", ("", 2, "\x00", lambda x: x)) 37 | ] 38 | 39 | _END_OF_PAYER_SORT, _END_OF_PAYER_TRANSFORMS = factor_transforms(_ITEMS) 40 | 41 | def xform(data): 42 | """ 43 | Applies transformation functions definted in _END_OF_PAYER_TRANSFORMS to 44 | data supplied as parameter to respective key-value pairs provided as the 45 | input parameter. 46 | 47 | Parameters 48 | ---------- 49 | data : dict 50 | Expects data parameter to have keys that exist in the 51 | _END_OF_PAYER_TRANSFORMS dict. 52 | 53 | Returns 54 | ---------- 55 | dict 56 | Dictionary containing processed (transformed) data provided as a 57 | parameter. 58 | """ 59 | return xform_entity(_END_OF_PAYER_TRANSFORMS, data) 60 | 61 | def fire(data): 62 | """ 63 | Returns the given record as a string formatted to the IRS Publication 1220 64 | specification, based on data supplied as parameter. 65 | 66 | Parameters 67 | ---------- 68 | data : dict 69 | Expects data parameter to have all keys specified in 70 | _END_OF_PAYER_TRANSFORMS. 71 | 72 | Returns 73 | ---------- 74 | str 75 | String formatted to meet IRS Publication 1220 76 | """ 77 | return fire_entity(_END_OF_PAYER_TRANSFORMS, _END_OF_PAYER_SORT, data) 78 | -------------------------------------------------------------------------------- /fire/entities/extension_of_time.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: entities.extension_of_time 3 | Representation of a "extension_of_time" record, including transformation 4 | functions and support functions for conversion into different formats. 5 | """ 6 | from fire.translator.util import digits_only, uppercase 7 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 8 | 9 | """ 10 | EXTENSION_OF_TIME_TRANSFORMS 11 | ----------------------- 12 | Stores metadata associated with each field in an Extension of Time record. 13 | Values in key-value pairs represent metadata in the following format: 14 | 15 | (default value, length, fill character, transformation function) 16 | """ 17 | 18 | _ITEMS = [ 19 | ("transmitter_control_code", ("", 5, "\x00", uppercase)), 20 | ("payer_tin", ("", 9, "\x00", digits_only)), 21 | ("first_payer_name", ("", 40, "\x00", uppercase)), 22 | ("second_payer_name", ("", 40, "\x00", uppercase)), 23 | ("payer_shipping_address", ("", 40, "\x00", uppercase)), 24 | ("payer_city", ("", 40, "\x00", uppercase)), 25 | ("payer_state", ("", 2, "\x00", uppercase)), 26 | ("payer_zip_code", ("", 9, "\x00", digits_only)), 27 | ("document_indicator", ("A", 1, "\x00", lambda x: x)), 28 | ("foreign_entity_indicator", ("", 1, "\x00", lambda x: x)), 29 | ("blank_1", ("", 11, "\x00", lambda x: x)), 30 | ("blank_2", ("", 2, "\x00", lambda x: x)) 31 | ] 32 | 33 | _EXTENSION_OF_TIME_SORT, _EXTENSION_OF_TIME_TRANSFORMS = \ 34 | factor_transforms(_ITEMS) 35 | 36 | def xform(data): 37 | """ 38 | Applies transformation functions definted in EXTENSION_OF_TIME_TRANSFORMS 39 | to data supplied as parameter to respective key-value pairs provided as the 40 | input parameter. 41 | 42 | Parameters 43 | ---------- 44 | data : dict 45 | Expects data parameter to have keys that exist in the 46 | EXTENSION_OF_TIME_TRANSFORMS dict. 47 | 48 | Returns 49 | ---------- 50 | dict 51 | Dictionary containing processed (transformed) data provided as a 52 | parameter. 53 | """ 54 | return xform_entity(_EXTENSION_OF_TIME_TRANSFORMS, data) 55 | 56 | def fire(data): 57 | """ 58 | Returns the given record as a string formatted to the IRS Publication 1220 59 | specification, based on data supplied as parameter. 60 | 61 | Parameters 62 | ---------- 63 | data : dict 64 | Expects data parameter to have all keys specified in 65 | EXTENSION_OF_TIME_TRANSFORMS. 66 | 67 | Returns 68 | ---------- 69 | str 70 | String formatted to meet IRS Publication 1220 71 | """ 72 | return fire_entity(_EXTENSION_OF_TIME_TRANSFORMS, 73 | _EXTENSION_OF_TIME_SORT, data, 200) 74 | -------------------------------------------------------------------------------- /fire/entities/payer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: entities.payer 3 | Representation of a "payer" record, including transformation functions 4 | and support functions for conversion into different formats. 5 | """ 6 | from fire.translator.util import digits_only, uppercase, rjust_zero 7 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 8 | 9 | """ 10 | _PAYER_TRANSFORMS 11 | ----------------------- 12 | Stores metadata associated with each field in a Transmitter record. 13 | Values in key-value pairs represent metadata in the following format: 14 | 15 | (default value, length, fill character, transformation function) 16 | """ 17 | 18 | _ITEMS = [ 19 | ("record_type", ("A", 1, "\x00", lambda x: x)), 20 | ("payment_year", ("", 4, "\x00", digits_only)), 21 | ("combined_fed_state", ("", 1, "\x00", lambda x: x)), 22 | ("blank_1", ("", 5, "\x00", lambda x: x)), 23 | ("payer_tin", ("", 9, "\x00", digits_only)), 24 | ("payer_name_control", ("", 4, "\x00", uppercase)), 25 | ("last_filing_indicator", ("", 1, "\x00", lambda x: x)), 26 | ("type_of_return", ("A", 2, "\x00", uppercase)), 27 | ("amount_codes", ("7", 16, "\x00", lambda x: x)), 28 | ("blank_2", ("", 8, "\x00", lambda x: x)), 29 | ("foreign_entity_indicator", ("", 1, "\x00", lambda x: x)), 30 | ("first_payer_name", ("", 40, "\x00", uppercase)), 31 | ("second_payer_name", ("", 40, "\x00", uppercase)), 32 | ("transfer_agent_control", ("0", 1, "\x00", lambda x: x)), 33 | ("payer_shipping_address", ("", 40, "\x00", uppercase)), 34 | ("payer_city", ("", 40, "\x00", uppercase)), 35 | ("payer_state", ("", 2, "\x00", uppercase)), 36 | ("payer_zip_code", ("", 9, "\x00", digits_only)), 37 | ("payer_telephone_number_and_ext", ("", 15, "\x00", digits_only)), 38 | ("blank_3", ("", 260, "\x00", lambda x: x)), 39 | ("record_sequence_number", 40 | ("00000002", 8, "\x00", lambda x: rjust_zero(x, 8))), 41 | ("blank_4", ("", 241, "\x00", lambda x: x)), 42 | ("blank_5", ("", 2, "\x00", lambda x: x)) 43 | ] 44 | 45 | _PAYER_SORT, _PAYER_TRANSFORMS = factor_transforms(_ITEMS) 46 | 47 | def xform(data): 48 | """ 49 | Applies transformation functions definted in _PAYER_TRANSFORMS to data 50 | supplied as parameter to respective key-value pairs provided as the 51 | input parameter. 52 | 53 | Parameters 54 | ---------- 55 | data : dict 56 | Expects data parameter to have keys that exist in the 57 | _PAYER_TRANSFORMS dict. 58 | 59 | Returns 60 | ---------- 61 | dict 62 | Dictionary containing processed (transformed) data provided as a 63 | parameter. 64 | """ 65 | return xform_entity(_PAYER_TRANSFORMS, data) 66 | 67 | def fire(data): 68 | """ 69 | Returns the given record as a string formatted to the IRS Publication 1220 70 | specification, based on data supplied as parameter. 71 | 72 | Parameters 73 | ---------- 74 | data : dict 75 | Expects data parameter to have all keys specified in _PAYER_TRANSFORMS. 76 | 77 | Returns 78 | ---------- 79 | str 80 | String formatted to meet IRS Publication 1220 81 | """ 82 | return fire_entity(_PAYER_TRANSFORMS, _PAYER_SORT, data) 83 | -------------------------------------------------------------------------------- /fire/entities/transmitter.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Module: entities.transmitter 4 | Representation of a "transmitter" record, including transformation functions 5 | and support functions for conversion into different formats. 6 | """ 7 | from fire.translator.util import digits_only, uppercase, rjust_zero 8 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 9 | 10 | """ 11 | _TRANSMITTER_TRANSFORMS 12 | ----------------------- 13 | Stores metadata associated with each field in a Transmitter record. 14 | Values in key-value pairs represent metadata in the following format: 15 | 16 | (default value, length, fill character, transformation function) 17 | """ 18 | 19 | _ITEMS = [ 20 | ("record_type", ("T", 1, "\x00", lambda x: x)), 21 | ("payment_year", ("0000", 4, "0", digits_only)), 22 | ("prior_year_data_indicator", ("", 1, "\x00", uppercase)), 23 | ("transmitter_tin", ("000000000", 9, "0", digits_only)), 24 | ("transmitter_control_code", ("", 5, "\x00", uppercase)), 25 | ("blank_1", ("", 7, "\x00", lambda x: x)), 26 | ("test_file_indicator", ("T", 1, "\x00", lambda x: x)), 27 | ("foreign_entity_indicator", ("", 1, "\x00", lambda x: x)), 28 | ("transmitter_name", ("", 40, "\x00", uppercase)), 29 | ("transmitter_name_contd", ("", 40, "\x00", uppercase)), 30 | ("company_name", ("", 40, "\x00", uppercase)), 31 | ("company_name_contd", ("", 40, "\x00", uppercase)), 32 | ("company_mailing_address", ("", 40, "\x00", uppercase)), 33 | ("company_city", ("", 40, "\x00", uppercase)), 34 | ("company_state", ("", 2, "\x00", uppercase)), 35 | ("company_zip_code", ("", 9, "\x00", digits_only)), 36 | ("blank_2", ("", 15, "\x00", lambda x: x)), 37 | ("total_number_of_payees", 38 | ("00000000", 8, "0", lambda x: rjust_zero(x, 8))), 39 | ("contact_name", ("", 40, "\x00", lambda x: x)), 40 | ("contact_telephone_number_and_ext", 41 | ("", 15, "\x00", digits_only)), 42 | ("contact_email_address", ("", 50, "\x00", lambda x: x)), 43 | ("blank_3", ("", 91, "\x00", lambda x: x)), 44 | ("record_sequence_number", ("", 8, "\x00", lambda x: x)), 45 | ("blank_4", ("", 10, "\x00", lambda x: x)), 46 | ("vendor_indicator", ("I", 1, "\x00", uppercase)), 47 | ("vendor_name", ("", 40, "\x00", uppercase)), 48 | ("vendor_mailing_address", ("", 40, "\x00", lambda x: x)), 49 | ("vendor_city", ("", 40, "\x00", uppercase)), 50 | ("vendor_state", ("", 2, "\x00", uppercase)), 51 | ("vendor_zip_code", ("", 9, "\x00", lambda x: x)), 52 | ("vendor_contact_name", ("", 40, "\x00", uppercase)), 53 | ("vendor_contact_telephone_and_ext", 54 | ("", 15, "\x00", digits_only)), 55 | ("blank_5", ("", 35, "\x00", lambda x: x)), 56 | ("vendor_foreign_entity_indicator", ("", 1, "\x00", uppercase)), 57 | ("blank_6", ("", 8, "\x00", lambda x: x)), 58 | ("blank_7", ("", 2, "\x00", lambda x: x)) 59 | ] 60 | 61 | _TRANSMITTER_SORT, _TRANSMITTER_TRANSFORMS = factor_transforms(_ITEMS) 62 | 63 | def xform(data): 64 | """ 65 | Applies transformation functions definted in _TRANSMITTER_TRANSFORMS to data 66 | supplied as parameter to respective key-value pairs provided as the 67 | input parameter. 68 | 69 | Parameters 70 | ---------- 71 | data : dict 72 | Expects data parameter to have keys that exist in the 73 | _TRANSMITTER_TRANSFORMS dict. 74 | 75 | Returns 76 | ---------- 77 | dict 78 | Dictionary containing processed (transformed) data provided as a 79 | parameter. 80 | """ 81 | return xform_entity(_TRANSMITTER_TRANSFORMS, data) 82 | 83 | def fire(data): 84 | """ 85 | Returns the given record as a string formatted to the IRS Publication 1220 86 | specification, based on data supplied as parameter. 87 | 88 | Parameters 89 | ---------- 90 | data : dict 91 | Expects data parameter to have all keys specified in 92 | _TRANSMITTER_TRANSFORMS. 93 | 94 | Returns 95 | ---------- 96 | str 97 | String formatted to meet IRS Publication 1220 98 | """ 99 | return fire_entity(_TRANSMITTER_TRANSFORMS, _TRANSMITTER_SORT, data) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What this is 2 | Fire-1099 helps you stop wasting ~$10 per page on 1099 filings. 3 | 4 | Specifically, it generates 1099 tax filings formatted for the IRS electronic filing system. 5 | 6 | A lot of small companies don't realize they have to file 1099s for most payments to lawyers, as well as independent contractors. The IRS has a system called "FIRE" for electronically submitting these filings, and others like stock options exercise forms. These filings can *only* be filed through this system. If you're used to modern REST APIs, you'll probably find FIRE unpleasant to use. It's inflexible, has an ambiguous spec, and operates on the byte (ASCII code) level. 7 | 8 | With fire-1099, you simply enter your form data in a JSON file [like this one](https://github.com/sdj0/fire-1099/blob/master/spec/data/valid_minimal_MISC.json) and run it through the program. It validates your data against the IRS spec, auto-formats it where possible, and writes it to a file that can be uploaded straight to FIRE. 9 | 10 | I should point out getting access to the FIRE system is non-trivial; it can take a couple of weeks. See below for a link to the form needed. 11 | 12 | # Using fire-1099 13 | To install the fire-1099 CLI, clone this repository and run the following command from the repository root directory: `pip install .` 14 | 15 | The CLI for generating FIRE-formatted files accepts three basic parameters: an input file path, an (optional) output file path, and the (optional) file type (--type). Type can be either NEC or MISC (default) 16 | 17 | 18 | `fire-1099 path/to/input-file.json --output path/to/output-file.ascii` 19 | 20 | `fire-1099 path/to/input-nec-file.json --output path/to/output-nec-file.ascii --type NEC` 21 | 22 | 23 | The input file should be JSON-formatted according to the schema defined in the `/schema` folder of this repo. The output file given by `--output` is optional, and will default to a timestamped filename in the same directory as the input file. Not all fields in the input file are required. I recommend using the file `/spec/data/valid-minimal.json` as a starting point if you're not comfortable with the schema file itself. 24 | 25 | 26 | ## API (Translator Module) 27 | As an alternative to the CLI, the `translator` module exposes a number of functions for generating FIRE-formatted files programatically. 28 | 29 | 30 | To run the file generation process end-to-end (similar to using the CLI), use `translator.run(str, str)`. Example: 31 | 32 | ```python 33 | import translator 34 | 35 | input_path = "/path/to/input_file.json" 36 | output_path = "/path/to/output_file.ascii" 37 | 38 | translator.run(input_path, output_path) 39 | ``` 40 | 41 | 42 | A more step-by-step interaction is also available: 43 | 44 | ```python 45 | import translator 46 | 47 | input_path = "/path/to/input_file.json" 48 | output_path = "/path/to/output_file.ascii" 49 | 50 | # Load input file and validate against schema 51 | user_data = extract_user_data(input_path) 52 | validate_user_data(user_data, schema_path) 53 | 54 | # Incorporate default values and system-generated data 55 | master = load_full_schema(user_data) 56 | insert_generated_values(master) 57 | 58 | # Generate ASCII string formatted to IRS 1220 spec, and write to file 59 | ascii_string = get_fire_format(master) 60 | write_1099_file(ascii_string, output_path) 61 | ``` 62 | 63 | 64 | # Access via IRS FIRE System 65 | A few things need to happen before you can submit an output file to the IRS: 66 | 67 | * You need a *Transmitter Control Code* or "TCC." This is done by filing From 4419 electronically (https://fire.irs.gov). It can take 45 days to get a response. 68 | * You need to have a valid business tax identification code (EIN/TIN). This will be linked to your TCC, and is what you'll use for the "transmitter" record in your FIRE submissions. 69 | 70 | # Future Work 71 | * Add support for "Extension of Time" requests 72 | * Add support for filings other than 1099-MISC and 1099-NEC 73 | * Improve schema regex validations 74 | * Add validation logic for more obscure fields, and for cross-field dependencies like "combined state-federal" 75 | * Add support for multiple sequential files, i.e. scope sequence numbers to multiple files 76 | 77 | 78 | Note: support multiple payers in a single file is not planned, as this is meant as a "DIY" tool for businesses to use themselves. 79 | -------------------------------------------------------------------------------- /fire/entities/payees.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: entities.payees 3 | Representation of a "payee" record, including transformation functions 4 | and support functions for conversion into different formats. 5 | 6 | Support functions are built to handle arrays of payees (as opposed to 7 | an individual payee) 8 | """ 9 | from itertools import chain 10 | 11 | from fire.translator.util import digits_only, uppercase, rjust_zero 12 | from fire.translator.util import factor_transforms, xform_entity, fire_entity 13 | """ 14 | _PAYEE_TRANSFORMS 15 | ----------------------- 16 | Stores metadata associated with each field in a Transmitter record. 17 | Values in key-value pairs represent metadata in the following format: 18 | 19 | (default value, length, fill character, transformation function) 20 | """ 21 | 22 | _ITEMS = [ 23 | ("record_type", ("B", 1, "\x00", lambda x: x)), 24 | ("payment_year", ("", 4, "\x00", lambda x: x)), 25 | ("corrected_return_indicator", ("", 1, "\x00", uppercase)), 26 | ("payees_name_control", ("", 4, "\x00", uppercase)), 27 | ("type_of_tin", ("1", 1, "\x00", lambda x: x)), 28 | ("payees_tin", ("000000000", 9, "\x00", digits_only)), 29 | ("payers_account_number_for_payee", ("", 20, "\x00", lambda x: x)), 30 | ("payers_office_code", ("", 4, "\x00", lambda x: x)), 31 | ("blank_1", ("", 10, "\x00", lambda x: x)) 32 | ] 33 | 34 | for field in chain((x for x in range(1, 10)), \ 35 | (chr(x) for x in range(ord('A'), ord('I'))), \ 36 | 'J'): 37 | _ITEMS.append((f"payment_amount_{field}", 38 | ("000000000000", 12, "\x00", lambda x: rjust_zero(x, 12)))) 39 | 40 | _ITEMS += [ 41 | ("blank_2", ("", 16, "\x00", lambda x: x)), 42 | ("foreign_country_indicator", ("", 1, "\x00", lambda x: x)), 43 | ("first_payee_name_line", ("", 40, "\x00", uppercase)), 44 | ("second_payee_name_line", ("", 40, "\x00", uppercase)), 45 | ("payee_mailing_address", ("", 40, "\x00", lambda x: x)), 46 | ("blank_3", ("", 40, "\x00", lambda x: x)), 47 | ("payee_city", ("", 40, "\x00", lambda x: x)), 48 | ("payee_state", ("", 2, "\x00", lambda x: x)), 49 | ("payee_zip_code", ("", 9, "\x00", lambda x: x)), 50 | ("blank_4", ("", 1, "\x00", lambda x: x)), 51 | ("record_sequence_number", 52 | ("00000003", 8, "\x00", lambda x: rjust_zero(x, 8))), 53 | ("blank_5", ("", 36, "\x00", lambda x: x)), 54 | ("second_tin_notice", ("", 1, "\x00", lambda x: x)), 55 | ("blank_6", ("", 2, "\x00", lambda x: x)), 56 | ("direct_sales_indicator", ("", 1, "\x00", lambda x: x)), 57 | ("fatca_filing_requirement_indicator", ("", 1, "\x00", lambda x: x)), 58 | ("blank_7", ("", 114, "\x00", lambda x: x)), 59 | ("special_data_entries", ("", 60, "\x00", lambda x: x)), 60 | ("state_income_tax_withheld", ("", 12, "\x00", lambda x: x)), 61 | ("local_income_tax_withheld", ("", 12, "\x00", lambda x: x)), 62 | ("combined_federal_state_code", ("", 2, "\x00", lambda x: x)), 63 | ("blank_8", ("", 2, "\x00", lambda x: x)) 64 | ] 65 | 66 | _PAYEE_SORT, _PAYEE_TRANSFORMS = factor_transforms(_ITEMS) 67 | 68 | def xform(data): 69 | """ 70 | Applies transformation functions definted in _PAYEE_TRANSFORMS to data 71 | supplied as parameter. 72 | 73 | Parameters 74 | ---------- 75 | data : array[dict] 76 | Array of dict elements containing Payee data. 77 | Expects element of the array to have keys that exist in the 78 | _PAYEE_TRANSFORMS dict (not required to have all keys). 79 | 80 | Returns 81 | ---------- 82 | dict 83 | Dictionary containing processed (transformed) data provided as a 84 | parameter. 85 | """ 86 | payees = [] 87 | for payee in data: 88 | payees.append(xform_entity(_PAYEE_TRANSFORMS, payee)) 89 | return payees 90 | 91 | def fire(data): 92 | """ 93 | Returns a string formatted to the IRS Publication 1220 specification based 94 | on data supplied as parameter. 95 | 96 | Parameters 97 | ---------- 98 | data : array[dict] 99 | Expects data elements to have all keys specified in _PAYEE_TRANSFORMS. 100 | 101 | Returns 102 | ---------- 103 | str 104 | String formatted to meet IRS Publication 1220 105 | """ 106 | payees_string = "" 107 | for payee in data: 108 | payees_string += fire_entity(_PAYEE_TRANSFORMS, _PAYEE_SORT, payee) 109 | return payees_string 110 | -------------------------------------------------------------------------------- /spec/test_entity_payer.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | 3 | from copy import deepcopy 4 | 5 | import jsonschema 6 | 7 | from jsonschema import validate 8 | from nose.tools import raises 9 | 10 | from spec_util import check_value_too_long, check_valid_phone_num, \ 11 | check_invalid_phone_num, check_valid_tin, \ 12 | check_invalid_tin, check_valid_zip, check_invalid_zip, \ 13 | SCHEMA, PAYER_BLANK_MAP, VALID_ALL_DATA, \ 14 | VALID_PHONE_NUMS, VALID_ZIPS, INVALID_ZIPS, \ 15 | INVALID_PHONE_NUMS, VALID_TINS, INVALID_TINS 16 | from fire.entities import payer 17 | 18 | VALID_PAYER = {} 19 | VALID_PAYER["payer"] = VALID_ALL_DATA["payer"] 20 | 21 | """ 22 | Schema validation tests: payer 23 | """ 24 | def test_payer_schema_ignore_extra_data(): 25 | temp = deepcopy(VALID_PAYER) 26 | temp["extraneous_key"] = "should_be_ignored" 27 | assert validate(temp, SCHEMA) is None, f"Object: {temp}" 28 | 29 | def test_payer_schema_overly_long_values(): 30 | temp = deepcopy(VALID_PAYER) 31 | for key, value in temp["payer"].items(): 32 | if isinstance(value, str): 33 | yield check_value_too_long, \ 34 | temp, ["payer", key], value + 99*"A" 35 | 36 | def test_payer_schema_phone_numbers(): 37 | temp = deepcopy(VALID_PAYER) 38 | for num in VALID_PHONE_NUMS: 39 | yield check_valid_phone_num, \ 40 | temp, ["payer", "payer_telephone_number_and_ext"], num 41 | 42 | for num in INVALID_PHONE_NUMS: 43 | yield check_invalid_phone_num, \ 44 | temp, ["payer", "payer_telephone_number_and_ext"], num 45 | 46 | def test_payer_schema_validation_tins(): 47 | temp = deepcopy(VALID_PAYER) 48 | for tin in VALID_TINS: 49 | yield check_valid_tin, \ 50 | temp, ["payer", "payer_tin"], tin 51 | for tin in INVALID_TINS: 52 | yield check_invalid_tin, \ 53 | temp, ["payer", "payer_tin"], tin 54 | 55 | def test_payer_schema_zip_codes(): 56 | temp = deepcopy(VALID_PAYER) 57 | for zip_code in VALID_ZIPS: 58 | yield check_valid_zip, \ 59 | temp, ["payer", "payer_zip_code"], zip_code 60 | for zip_code in INVALID_ZIPS: 61 | yield check_invalid_zip, \ 62 | temp, ["payer", "payer_zip_code"], zip_code 63 | 64 | @raises(jsonschema.exceptions.ValidationError) 65 | def test_missing_required_data(): 66 | temp = deepcopy(VALID_PAYER) 67 | del temp["payer"]["payer_tin"] 68 | validate(temp, SCHEMA) 69 | 70 | """ 71 | User data transformation tests: payer.xform() 72 | """ 73 | def test_payer_xform_uppercase(): 74 | temp = deepcopy(VALID_PAYER) 75 | temp["payer"]["first_payer_name"] = "nocaps mclowercase" 76 | transformed = payer.xform(temp["payer"]) 77 | assert transformed["first_payer_name"] == "NOCAPS MCLOWERCASE" 78 | 79 | def test_payer_xform_remove_punctuation(): 80 | temp = deepcopy(VALID_PAYER) 81 | temp["payer"]["payer_telephone_number_and_ext"] = "(555)555-5555" 82 | transformed = payer.xform(temp["payer"]) 83 | assert transformed["payer_telephone_number_and_ext"] == "5555555555" 84 | 85 | def test_payer_xform_adds_system_fields(): 86 | temp = deepcopy(VALID_PAYER) 87 | assert "blank_2" not in temp 88 | transformed = payer.xform(temp) 89 | assert "blank_2" in transformed 90 | 91 | """ 92 | FIRE-formatted ASCII string generation tests: payer.fire() 93 | """ 94 | def test_payer_fire_string_length(): 95 | temp = deepcopy(VALID_PAYER) 96 | transformed = payer.xform(temp["payer"]) 97 | test_string = payer.fire(transformed) 98 | assert len(test_string) == 750 99 | 100 | def test_payer_fire_padding_blanks(): 101 | temp = deepcopy(VALID_PAYER) 102 | temp["payer"]["payer_shipping_address"] = "1234 ROADSTREET AVE" 103 | transformed = payer.xform(temp["payer"]) 104 | test_string = payer.fire(transformed) 105 | addr = test_string[133:173] 106 | 107 | assert addr[0:19] == "1234 ROADSTREET AVE" 108 | assert addr[19:] == 21*"\x00" 109 | 110 | def test_payer_fire_padding_zeros(): 111 | temp = deepcopy(VALID_PAYER) 112 | temp["payer"]["record_sequence_number"] = "2" 113 | transformed = payer.xform(temp["payer"]) 114 | test_string = payer.fire(transformed) 115 | sequence_num = test_string[499:507] 116 | assert sequence_num == "00000002" 117 | 118 | def test_payer_fire_blanks_layout(): 119 | temp = deepcopy(VALID_PAYER) 120 | transformed = payer.xform(temp["payer"]) 121 | test_string = payer.fire(transformed) 122 | for (offset_1_indexed, inclusive_bound) in PAYER_BLANK_MAP: 123 | yield check_blanks, test_string[(offset_1_indexed -1):inclusive_bound] 124 | 125 | def check_blanks(sub_string): 126 | assert sub_string == len(sub_string)*"\x00" 127 | -------------------------------------------------------------------------------- /spec/data/valid_all_MISC.json: -------------------------------------------------------------------------------- 1 | { 2 | "transmitter":{ 3 | "record_type": "T", 4 | "payment_year": "2017", 5 | "prior_year_data_indicator": "", 6 | "transmitter_tin": "123456789", 7 | "transmitter_control_code": "55AA5", 8 | "test_file_indicator": "T", 9 | "foreign_entity_indicator": "X", 10 | "transmitter_name": "ASDF GLOBAL INC", 11 | "transmitter_name_contd": "", 12 | "company_name": "ASDF GLOBAL INC", 13 | "company_name_contd": "", 14 | "company_mailing_address": "123 ASDF STREET", 15 | "company_city": "NEW YORK", 16 | "company_state": "NY", 17 | "company_zip_code": "10001", 18 | "total_number_of_payees": "2", 19 | "contact_name": "RONALD SWANSON", 20 | "contact_telephone_number_and_ext": "555 555 5555", 21 | "contact_email_address": "ronald@swanson.com", 22 | "record_sequence_number": "00000001", 23 | "vendor_indicator": "1", 24 | "vendor_name": "GSG Corp", 25 | "vendor_mailing_address": "1234 POIU St", 26 | "vendor_city": "TAXVILLE", 27 | "vendor_state": "TX", 28 | "vendor_zip_code": "10991", 29 | "vendor_contact_name": "BLERD FLERPLERMERD", 30 | "vendor_contact_telephone_and_ext": "5557776666", 31 | "vendor_foreign_entity_indicator": "1" 32 | }, 33 | "payer":{ 34 | "payment_year": "2017", 35 | "combined_fed_state": "1", 36 | "payer_tin": "123456789", 37 | "payer_name_control": "ASDF", 38 | "last_filing_indicator": "1", 39 | "type_of_return": "A", 40 | "amount_codes": "7", 41 | "foreign_entity_indicator": "1", 42 | "first_payer_name": "ASDF GLOBAL INC", 43 | "second_payer_name": "", 44 | "transfer_agent_control": "", 45 | "payer_shipping_address": "123 ASDF STREET", 46 | "payer_city": "NEW YORK", 47 | "payer_state": "NY", 48 | "payer_zip_code": "10001", 49 | "payer_telephone_number_and_ext": "5555555555", 50 | "record_sequence_number": "00000002" 51 | }, 52 | "payees":[ 53 | { 54 | "record_type": "B", 55 | "payment_year": "2017", 56 | "corrected_return_indicator": "", 57 | "payees_name_control": "SPAC", 58 | "type_of_tin": "1", 59 | "payees_tin": "987654321", 60 | "payers_account_number_for_payee": "", 61 | "payers_office_code": "", 62 | "payment_amount_1": "100", 63 | "payment_amount_2": "200", 64 | "payment_amount_3": "300", 65 | "payment_amount_4": "400", 66 | "payment_amount_5": "500", 67 | "payment_amount_6": "600", 68 | "payment_amount_7": "700", 69 | "payment_amount_8": "800", 70 | "payment_amount_9": "900", 71 | "payment_amount_A": "1000", 72 | "payment_amount_B": "1100", 73 | "payment_amount_C": "1200", 74 | "payment_amount_D": "1300", 75 | "payment_amount_E": "1400", 76 | "payment_amount_F": "1500", 77 | "payment_amount_G": "1600", 78 | "foreign_country_indicator": "", 79 | "first_payee_name_line": "SPACELEY SPROCKETS", 80 | "second_payee_name_line": "", 81 | "payee_mailing_address": "5678 INDUSTRY PLACE", 82 | "payee_city": "MOON", 83 | "payee_state": "CA", 84 | "payee_zip_code": "22222", 85 | "record_sequence_number": "00000003", 86 | "second_tin_notice": "", 87 | "direct_sales_indicator": "", 88 | "fatca_filing_requirement_indicator": "", 89 | "special_data_entries": "", 90 | "state_income_tax_withheld": "", 91 | "local_income_tax_withheld": "", 92 | "combined_federal_state_code": "" 93 | }, 94 | { 95 | "record_type": "B", 96 | "payment_year": "2017", 97 | "corrected_return_indicator": "", 98 | "payees_name_control": "BOBL", 99 | "type_of_tin": "1", 100 | "payees_tin": "098765432", 101 | "payers_account_number_for_payee": "", 102 | "payers_office_code": "", 103 | "payment_amount_1": "1600", 104 | "payment_amount_2": "1500", 105 | "payment_amount_3": "1400", 106 | "payment_amount_4": "1300", 107 | "payment_amount_5": "1200", 108 | "payment_amount_6": "1100", 109 | "payment_amount_7": "1000", 110 | "payment_amount_8": "900", 111 | "payment_amount_9": "800", 112 | "payment_amount_A": "700", 113 | "payment_amount_B": "600", 114 | "payment_amount_C": "500", 115 | "payment_amount_D": "400", 116 | "payment_amount_E": "300", 117 | "payment_amount_F": "200", 118 | "payment_amount_G": "100", 119 | "foreign_country_indicator": "", 120 | "first_payee_name_line": "BOB LOBLAW LLP", 121 | "second_payee_name_line": "", 122 | "payee_mailing_address": "100 LAWBOMB RD", 123 | "payee_city": "BLOBVILLE", 124 | "payee_state": "CA", 125 | "payee_zip_code": "11111", 126 | "record_sequence_number": "00000004", 127 | "second_tin_notice": "", 128 | "direct_sales_indicator": "", 129 | "fatca_filing_requirement_indicator": "", 130 | "special_data_entries": "", 131 | "state_income_tax_withheld": "", 132 | "local_income_tax_withheld": "", 133 | "combined_federal_state_code": "" 134 | } 135 | ] 136 | } 137 | 138 | -------------------------------------------------------------------------------- /spec/data/valid_standard_MISC.json: -------------------------------------------------------------------------------- 1 | { 2 | "transmitter":{ 3 | "record_type": "T", 4 | "payment_year": "2017", 5 | "prior_year_data_indicator": "", 6 | "transmitter_tin": "123456789", 7 | "transmitter_control_code": "55AA5", 8 | "test_file_indicator": "T", 9 | "foreign_entity_indicator": "X", 10 | "transmitter_name": "ASDF GLOBAL INC", 11 | "transmitter_name_contd": "", 12 | "company_name": "ASDF GLOBAL INC", 13 | "company_name_contd": "", 14 | "company_mailing_address": "123 ASDF STREET", 15 | "company_city": "NEW YORK", 16 | "company_state": "NY", 17 | "company_zip_code": "10001", 18 | "total_number_of_payees": "2", 19 | "contact_name": "RONALD SWANSON", 20 | "contact_telephone_number_and_ext": "5555555555", 21 | "contact_email_address": "ronald@swanson.com", 22 | "record_sequence_number": "00000001", 23 | "vendor_indicator": "1", 24 | "vendor_name": "GSG Corp", 25 | "vendor_mailing_address": "1234 POIU St", 26 | "vendor_city": "TAXVILLE", 27 | "vendor_state": "TX", 28 | "vendor_zip_code": "10991", 29 | "vendor_contact_name": "BLERD FLERPLERMERD", 30 | "vendor_contact_telephone_and_ext": "5557776666", 31 | "vendor_foreign_entity_indicator": "1" 32 | }, 33 | "payer":{ 34 | "payment_year": "2017", 35 | "combined_fed_state": "1", 36 | "payer_tin": "123456789", 37 | "payer_name_control": "ASDF", 38 | "last_filing_indicator": "1", 39 | "type_of_return": "A", 40 | "amount_codes": "7", 41 | "foreign_entity_indicator": "1", 42 | "first_payer_name": "ASDF GLOBAL INC", 43 | "second_payer_name": "", 44 | "transfer_agent_control": "", 45 | "payer_shipping_address": "123 ASDF STREET", 46 | "payer_city": "NEW YORK", 47 | "payer_state": "NY", 48 | "payer_zip_code": "10001", 49 | "payer_telephone_number_and_ext": "5555555555", 50 | "record_sequence_number": "00000002" 51 | }, 52 | "payees":[ 53 | { 54 | "record_type": "B", 55 | "payment_year": "2017", 56 | "corrected_return_indicator": "", 57 | "payees_name_control": "SPAC", 58 | "type_of_tin": "1", 59 | "payees_tin": "987654321", 60 | "payers_account_number_for_payee": "", 61 | "payers_office_code": "", 62 | "payment_amount_1": "100", 63 | "payment_amount_2": "200", 64 | "payment_amount_3": "300", 65 | "payment_amount_4": "400", 66 | "payment_amount_5": "500", 67 | "payment_amount_6": "600", 68 | "payment_amount_7": "700", 69 | "payment_amount_8": "800", 70 | "payment_amount_9": "900", 71 | "payment_amount_A": "1000", 72 | "payment_amount_B": "1100", 73 | "payment_amount_C": "1200", 74 | "payment_amount_D": "1300", 75 | "payment_amount_E": "1400", 76 | "payment_amount_F": "1500", 77 | "payment_amount_G": "1600", 78 | "foreign_country_indicator": "", 79 | "first_payee_name_line": "SPACELEY SPROCKETS", 80 | "second_payee_name_line": "", 81 | "payee_mailing_address": "5678 INDUSTRY PLACE", 82 | "payee_city": "MOON", 83 | "payee_state": "CA", 84 | "payee_zip_code": "22222", 85 | "record_sequence_number": "00000003", 86 | "second_tin_notice": "", 87 | "direct_sales_indicator": "", 88 | "fatca_filing_requirement_indicator": "", 89 | "special_data_entries": "", 90 | "state_income_tax_withheld": "", 91 | "local_income_tax_withheld": "", 92 | "combined_federal_state_code": "" 93 | }, 94 | { 95 | "record_type": "B", 96 | "payment_year": "2017", 97 | "corrected_return_indicator": "", 98 | "payees_name_control": "BOBL", 99 | "type_of_tin": "1", 100 | "payees_tin": "098765432", 101 | "payers_account_number_for_payee": "", 102 | "payers_office_code": "", 103 | "payment_amount_1": "1600", 104 | "payment_amount_2": "1500", 105 | "payment_amount_3": "1400", 106 | "payment_amount_4": "1300", 107 | "payment_amount_5": "1200", 108 | "payment_amount_6": "1100", 109 | "payment_amount_7": "1000", 110 | "payment_amount_8": "900", 111 | "payment_amount_9": "800", 112 | "payment_amount_A": "700", 113 | "payment_amount_B": "600", 114 | "payment_amount_C": "500", 115 | "payment_amount_D": "400", 116 | "payment_amount_E": "300", 117 | "payment_amount_F": "200", 118 | "payment_amount_G": "100", 119 | "foreign_country_indicator": "", 120 | "first_payee_name_line": "BOB LOBLAW LLP", 121 | "second_payee_name_line": "", 122 | "payee_mailing_address": "100 LAWBOMB RD", 123 | "payee_city": "BLOBVILLE", 124 | "payee_state": "CA", 125 | "payee_zip_code": "11111", 126 | "record_sequence_number": "00000004", 127 | "second_tin_notice": "", 128 | "direct_sales_indicator": "", 129 | "fatca_filing_requirement_indicator": "", 130 | "special_data_entries": "", 131 | "state_income_tax_withheld": "", 132 | "local_income_tax_withheld": "", 133 | "combined_federal_state_code": "" 134 | } 135 | ] 136 | } 137 | 138 | -------------------------------------------------------------------------------- /spec/test_entity_payee.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | 3 | import re 4 | 5 | from copy import deepcopy 6 | 7 | import jsonschema 8 | 9 | from jsonschema import validate 10 | from nose.tools import raises 11 | 12 | from spec_util import VALID_ALL_DATA, SCHEMA, check_valid_amount, \ 13 | check_invalid_amount, check_value_too_long, \ 14 | check_valid_tin, check_invalid_tin, check_valid_zip, \ 15 | check_invalid_zip, check_blanks, \ 16 | VALID_TINS, INVALID_TINS, VALID_ZIPS, INVALID_ZIPS, \ 17 | PAYEE_BLANK_MAP, VALID_DOLLAR_AMOUNTS, \ 18 | INVALID_DOLLAR_AMOUNTS 19 | from fire.entities import payees 20 | 21 | VALID_PAYEE = [] 22 | VALID_PAYEE = VALID_ALL_DATA["payees"] 23 | 24 | 25 | """ 26 | Schema validation tests: payee 27 | """ 28 | def test_payee_schema_ignore_extra_data(): 29 | temp = {} 30 | temp["payees"] = deepcopy(VALID_PAYEE) 31 | temp["payees"][0]["extraneous_key"] = "should_be_ignored" 32 | assert validate(temp, SCHEMA) is None, f"Object: {temp}" 33 | 34 | def test_payee_schema_overly_long_values(): 35 | temp = {} 36 | temp["payees"] = deepcopy(VALID_PAYEE) 37 | for i, payee in enumerate(temp["payees"]): 38 | for key, value in payee.items(): 39 | if isinstance(value, str): 40 | yield check_value_too_long, \ 41 | temp, ["payees", i, key], value + 99*"A" 42 | 43 | def test_payee_schema_amount_codes(): 44 | temp = {} 45 | temp["payees"] = deepcopy(VALID_PAYEE) 46 | for i, payee in enumerate(temp["payees"]): 47 | for key in payee.keys(): 48 | if re.match(r"^payment_amount_", key): 49 | payload = ["payees", i, key] 50 | for amount in VALID_DOLLAR_AMOUNTS: 51 | yield check_valid_amount, temp, payload, amount 52 | for amount in INVALID_DOLLAR_AMOUNTS: 53 | yield check_invalid_amount, temp, payload, amount 54 | 55 | def test_payee_schema_validation_tins(): 56 | temp = {} 57 | temp["payees"] = deepcopy(VALID_PAYEE) 58 | for i, _ in enumerate(temp["payees"]): 59 | for tin in VALID_TINS: 60 | yield check_valid_tin, \ 61 | temp, ["payees", i, "payees_tin"], tin 62 | for tin in INVALID_TINS: 63 | yield check_invalid_tin, \ 64 | temp, ["payees", i, "payees_tin"], tin 65 | 66 | def test_payee_schema_zip_codes(): 67 | temp = {} 68 | temp["payees"] = deepcopy(VALID_PAYEE) 69 | for i, _ in enumerate(temp["payees"]): 70 | for zip_code in VALID_ZIPS: 71 | yield check_valid_zip, \ 72 | temp, ["payees", i, "payee_zip_code"], zip_code 73 | for zip_code in INVALID_ZIPS: 74 | yield check_invalid_zip, \ 75 | temp, ["payees", i, "payee_zip_code"], zip_code 76 | 77 | @raises(jsonschema.exceptions.ValidationError) 78 | def test_missing_required_data(): 79 | temp = {} 80 | temp["payees"] = deepcopy(VALID_PAYEE) 81 | del temp["payees"][0]["payees_tin"] 82 | validate(temp, SCHEMA) 83 | 84 | """ 85 | User data transformation tests: payees.xform() 86 | """ 87 | def test_payee_xform_uppercase(): 88 | temp = deepcopy(VALID_PAYEE) 89 | temp[0]["first_payee_name_line"] = "nocaps mclowercase" 90 | transformed = payees.xform(temp) 91 | assert transformed[0]["first_payee_name_line"] == "NOCAPS MCLOWERCASE" 92 | 93 | def test_payee_xform_remove_punctuation(): 94 | temp = deepcopy(VALID_PAYEE) 95 | temp[0]["payees_tin"] = "12-1234567" 96 | transformed = payees.xform(temp) 97 | assert transformed[0]["payees_tin"] == "121234567" 98 | 99 | def test_payee_xform_adds_system_fields(): 100 | temp = deepcopy(VALID_PAYEE) 101 | assert "blank_2" not in temp 102 | transformed = payees.xform(temp) 103 | assert "blank_2" in transformed[0] 104 | 105 | """ 106 | FIRE-formatted ASCII string generation tests: payees.fire() 107 | """ 108 | def test_payee_fire_string_length(): 109 | temp = deepcopy(VALID_PAYEE) 110 | transformed = payees.xform(temp) 111 | test_string = payees.fire(transformed) 112 | assert len(test_string) == 750*len(temp) 113 | 114 | def test_payee_fire_padding_blanks(): 115 | temp = deepcopy(VALID_PAYEE) 116 | temp[0]["payee_mailing_address"] = "1234 ROADSTREET AVE" 117 | transformed = payees.xform(temp) 118 | test_string = payees.fire(transformed) 119 | addr = test_string[367:407] 120 | 121 | assert addr[0:19] == "1234 ROADSTREET AVE" 122 | assert addr[19:] == 21*"\x00" 123 | 124 | def test_payee_fire_padding_zeros(): 125 | temp = deepcopy(VALID_PAYEE) 126 | temp[0]["record_sequence_number"] = "2" 127 | transformed = payees.xform(temp) 128 | test_string = payees.fire(transformed) 129 | sequence_num = test_string[499:507] 130 | assert sequence_num == "00000002" 131 | 132 | def test_payee_fire_blanks_layout(): 133 | temp = deepcopy(VALID_PAYEE) 134 | transformed = payees.xform(temp) 135 | test_string = payees.fire(transformed) 136 | for (offset_1_indexed, inclusive_bound) in PAYEE_BLANK_MAP: 137 | yield check_blanks, test_string[(offset_1_indexed -1):inclusive_bound] 138 | -------------------------------------------------------------------------------- /spec/spec_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions used in test specs. 3 | """ 4 | 5 | # pylint: disable=missing-docstring 6 | 7 | import json 8 | 9 | from copy import deepcopy 10 | 11 | import jsonschema 12 | 13 | from jsonschema import validate 14 | from nose.tools import raises 15 | 16 | SCHEMA = json.load(open("./fire/schema/base_schema.json")) 17 | VALID_ALL_PATH = "./spec/data/valid_all.json" 18 | INVALID_PHONE_NUMS = ["+1 555 666 7777", "555 A44 B777", "123ABC5678", 19 | "123 45 67", "555 666 777788", "555 666 7777 #"] 20 | VALID_PHONE_NUMS = ["5556667777", "555-666-7777", "(555)666-7777", 21 | "(555) 666 7777", "555.666.7777", "(555)-666-7777", 22 | "(555) 666-7777"] 23 | VALID_TINS = ["12-1234567", "121234567", "12 1234567", 24 | "111-11-1111", "222 22 2222"] 25 | INVALID_TINS = ["12-12345678", "12-123456", "12-12345-6", "12-ABCDEFG", 26 | "111-1-1111", "111-111-111", "1111-11-1111"] 27 | 28 | VALID_EMAILS = ["ASDF@asdf.com", "test@test.test.test", "abc1234_234@a.ccc"] 29 | INVALID_EMAILS = ["asdf@asdf@asdf.com", "noat.com", "@noname.com", "nodomain@", 30 | "test@rootonly"] 31 | 32 | VALID_ZIPS = ["10013", "10013-1001", "11111 2020", "111111111"] 33 | INVALID_ZIPS = ["1001", "11111-11111", "1111111111", "11-11"] 34 | 35 | VALID_DOLLAR_AMOUNTS = ["1234500", "12345.00", "1,234,567.00", "$1.00", "$.01", 36 | ".01", "0.01", "123,45600"] 37 | INVALID_DOLLAR_AMOUNTS = ["ABCD", "$", "&123,456.00", "€1.00", "0$12.01", 38 | "A$12.01" "€100"] 39 | 40 | # Map of offsets that should be blank in transmitter ASCII string. Format: 41 | # (first_byte:last_byte) note that references are 1-indexed 42 | TRANSMITTER_BLANK_MAP = [ 43 | (21, 27), 44 | (281, 295), 45 | (409, 499), 46 | (508, 517), 47 | (705, 739), 48 | (741, 750) 49 | ] 50 | 51 | PAYER_BLANK_MAP = [ 52 | (7, 11), 53 | (44, 51), 54 | (240, 499), 55 | (508, 750) 56 | ] 57 | 58 | PAYEE_BLANK_MAP = [ 59 | (45, 54), 60 | (328, 367), 61 | (408, 447), 62 | (499, 499), 63 | (508, 543), 64 | (723, 750) 65 | ] 66 | 67 | END_OF_PAYER_BLANK_MAP = [ 68 | (10, 15), 69 | (304, 499), 70 | (508, 750) 71 | ] 72 | 73 | END_OF_TRANSMISSION_BLANK_MAP = [ 74 | (10, 15), 75 | (304, 499), 76 | (508, 706), 77 | (743, 746), 78 | (749, 750) 79 | ] 80 | 81 | VALID_ALL_DATA = {} 82 | with open(VALID_ALL_PATH, mode='r', encoding='utf-8') as valid_all_file: 83 | VALID_ALL_DATA = json.load(valid_all_file) 84 | 85 | """ 86 | Locates a key-value pair a multi-tier dict, and overwrites the value 87 | at that with the provided value. 88 | 89 | Parameters: 90 | ----------- 91 | full_dictionary: dict 92 | The dictionary containing the value to be modified. 93 | 94 | path: list 95 | List containing the path of the key to be modified, e.g. 96 | ["top_level_object", "second_level_object", "key_to_be_modified"]. 97 | 98 | value: object 99 | The new value to insert at the key given by 'path' parameter. 100 | 101 | Returns: 102 | ----------- 103 | dict 104 | New dictionary wherein the key specified by 'path' parameter has the value 105 | specified by the 'value' parameter. 106 | """ 107 | def dive_to_path(full_dictionary, path, value): 108 | def _dive_recursion(sub_dict, path, value): 109 | if len(path) > 1: 110 | sub_dict[path[0]] = \ 111 | _dive_recursion(sub_dict[path[0]], path[1:], value) 112 | if len(path) == 1: 113 | sub_dict[path[0]] = value 114 | return sub_dict 115 | return sub_dict 116 | dict_copy = deepcopy(full_dictionary) 117 | return _dive_recursion(dict_copy, path, value) 118 | 119 | def check_blanks(sub_string): 120 | assert sub_string == len(sub_string)*"\x00" 121 | 122 | @raises(jsonschema.exceptions.ValidationError) 123 | def check_value_too_long(dict_obj, path, value): 124 | temp_obj = dive_to_path(dict_obj, path, value) 125 | validate(temp_obj, SCHEMA) 126 | 127 | @raises(jsonschema.exceptions.ValidationError) 128 | def check_invalid_phone_num(dict_obj, path, num): 129 | temp_obj = dive_to_path(dict_obj, path, num) 130 | validate(temp_obj, SCHEMA) 131 | 132 | def check_valid_phone_num(dict_obj, path, num): 133 | temp_obj = dive_to_path(dict_obj, path, num) 134 | assert validate(temp_obj, SCHEMA) is None, \ 135 | f"Phone number: {num}, Object: {dict_obj}" 136 | 137 | def check_valid_tin(dict_obj, path, tin): 138 | dive_to_path(dict_obj, path, tin) 139 | assert validate(dict_obj, SCHEMA) is None, f"TIN: {tin}, Object: {dict_obj}" 140 | 141 | @raises(jsonschema.exceptions.ValidationError) 142 | def check_invalid_tin(dict_obj, path, tin): 143 | temp_obj = dive_to_path(dict_obj, path, tin) 144 | validate(temp_obj, SCHEMA) 145 | 146 | def check_valid_email(dict_obj, path, email): 147 | dive_to_path(dict_obj, path, email) 148 | assert validate(dict_obj, SCHEMA) is None, \ 149 | f"Email: {email}, Object: {dict_obj}" 150 | 151 | @raises(jsonschema.exceptions.ValidationError) 152 | def check_invalid_email(dict_obj, path, email): 153 | temp_obj = dive_to_path(dict_obj, path, email) 154 | validate(temp_obj, SCHEMA) 155 | 156 | def check_valid_zip(dict_obj, path, zip_code): 157 | dive_to_path(dict_obj, path, zip_code) 158 | assert validate(dict_obj, SCHEMA) is None, \ 159 | f"Zip Code: {zip_code}, Object: {dict_obj}" 160 | 161 | @raises(jsonschema.exceptions.ValidationError) 162 | def check_invalid_zip(dict_obj, path, zip_code): 163 | temp_obj = dive_to_path(dict_obj, path, zip_code) 164 | validate(temp_obj, SCHEMA) 165 | 166 | def check_valid_amount(dict_obj, path, dollar_amount): 167 | dive_to_path(dict_obj, path, dollar_amount) 168 | assert validate(dict_obj, SCHEMA) is None, \ 169 | f"Dollar Amount: {dollar_amount}, Object: {dict_obj}" 170 | 171 | @raises(jsonschema.exceptions.ValidationError) 172 | def check_invalid_amount(dict_obj, path, dollar_amount): 173 | temp_obj = dive_to_path(dict_obj, path, dollar_amount) 174 | validate(temp_obj, SCHEMA) 175 | -------------------------------------------------------------------------------- /spec/test_entity_transmitter.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | 3 | from copy import deepcopy 4 | 5 | import jsonschema 6 | 7 | from jsonschema import validate 8 | from nose.tools import raises 9 | 10 | from spec_util import check_value_too_long, check_valid_phone_num, \ 11 | check_invalid_phone_num, check_valid_tin, \ 12 | check_invalid_tin, check_valid_zip, check_invalid_zip, \ 13 | check_valid_email, check_invalid_email, \ 14 | SCHEMA, TRANSMITTER_BLANK_MAP, VALID_ALL_DATA, \ 15 | VALID_PHONE_NUMS, VALID_ZIPS, INVALID_ZIPS, \ 16 | VALID_EMAILS, INVALID_EMAILS, INVALID_PHONE_NUMS, \ 17 | VALID_TINS, INVALID_TINS 18 | from fire.entities import transmitter 19 | 20 | VALID_TRANSMITTER = {} 21 | VALID_TRANSMITTER["transmitter"] = VALID_ALL_DATA["transmitter"] 22 | 23 | """ 24 | Schema validation tests: transmitter 25 | """ 26 | def test_transmitter_schema_ignore_extra_data(): 27 | temp = deepcopy(VALID_TRANSMITTER) 28 | temp["extraneous_key"] = "should_be_ignored" 29 | assert validate(temp, SCHEMA) is None, f"Object: {temp}" 30 | 31 | def test_transmitter_schema_overly_long_values(): 32 | temp = deepcopy(VALID_TRANSMITTER) 33 | for key, value in temp["transmitter"].items(): 34 | if isinstance(value, str): 35 | yield check_value_too_long, \ 36 | temp, ["transmitter", key], value + 99*"A" 37 | 38 | def test_transmitter_schema_phone_numbers(): 39 | temp = deepcopy(VALID_TRANSMITTER) 40 | for num in VALID_PHONE_NUMS: 41 | yield check_valid_phone_num, \ 42 | temp, ["transmitter", "contact_telephone_number_and_ext"], num 43 | yield check_valid_phone_num, \ 44 | temp, ["transmitter", "vendor_contact_telephone_and_ext"], num 45 | 46 | for num in INVALID_PHONE_NUMS: 47 | yield check_invalid_phone_num, \ 48 | temp, ["transmitter", "contact_telephone_number_and_ext"], num 49 | yield check_invalid_phone_num, \ 50 | temp, ["transmitter", "vendor_contact_telephone_and_ext"], num 51 | 52 | def test_transmitter_schema_validation_tins(): 53 | temp = deepcopy(VALID_TRANSMITTER) 54 | for tin in VALID_TINS: 55 | yield check_valid_tin, \ 56 | temp, ["transmitter", "transmitter_tin"], tin 57 | for tin in INVALID_TINS: 58 | yield check_invalid_tin, \ 59 | temp, ["transmitter", "transmitter_tin"], tin 60 | 61 | def test_transmitter_schema_emails(): 62 | temp = deepcopy(VALID_TRANSMITTER) 63 | for email in VALID_EMAILS: 64 | yield check_valid_email, \ 65 | temp, ["transmitter", "contact_email_address"], email 66 | for email in INVALID_EMAILS: 67 | yield check_invalid_email, \ 68 | temp, ["transmitter", "contact_email_address"], email 69 | 70 | def test_transmitter_schema_zip_codes(): 71 | temp = deepcopy(VALID_TRANSMITTER) 72 | for zip_code in VALID_ZIPS: 73 | yield check_valid_zip, \ 74 | temp, ["transmitter", "company_zip_code"], zip_code 75 | yield check_valid_zip, \ 76 | temp, ["transmitter", "vendor_zip_code"], zip_code 77 | for zip_code in INVALID_ZIPS: 78 | yield check_invalid_zip, \ 79 | temp, ["transmitter", "company_zip_code"], zip_code 80 | yield check_invalid_zip, \ 81 | temp, ["transmitter", "vendor_zip_code"], zip_code 82 | 83 | @raises(jsonschema.exceptions.ValidationError) 84 | def test_missing_required_data(): 85 | temp = deepcopy(VALID_TRANSMITTER) 86 | del temp["transmitter"]["transmitter_tin"] 87 | validate(temp, SCHEMA) 88 | 89 | """ 90 | User data transformation tests: transmitter.xform() 91 | """ 92 | def test_transmitter_xform_uppercase(): 93 | temp = deepcopy(VALID_TRANSMITTER) 94 | temp["transmitter"]["transmitter_name"] = "nocaps mclowercase" 95 | transformed = transmitter.xform(temp["transmitter"]) 96 | assert transformed["transmitter_name"] == "NOCAPS MCLOWERCASE" 97 | 98 | def test_transmitter_xform_remove_punctuation(): 99 | temp = deepcopy(VALID_TRANSMITTER) 100 | temp["transmitter"]["contact_telephone_number_and_ext"] = "(555)555-5555" 101 | transformed = transmitter.xform(temp["transmitter"]) 102 | assert transformed["contact_telephone_number_and_ext"] == "5555555555" 103 | 104 | def test_transmitter_xform_adds_system_fields(): 105 | temp = deepcopy(VALID_TRANSMITTER) 106 | assert "blank_2" not in temp 107 | transformed = transmitter.xform(temp) 108 | assert "blank_2" in transformed 109 | 110 | """ 111 | FIRE-formatted ASCII string generation tests: transmitter.fire() 112 | """ 113 | def test_transmitter_fire_string_length(): 114 | temp = deepcopy(VALID_TRANSMITTER) 115 | transformed = transmitter.xform(temp["transmitter"]) 116 | test_string = transmitter.fire(transformed) 117 | assert len(test_string) == 750 118 | 119 | def test_transmitter_fire_padding_blanks(): 120 | temp = deepcopy(VALID_TRANSMITTER) 121 | temp["transmitter"]["company_mailing_address"] = "1234 ROADSTREET AVE" 122 | transformed = transmitter.xform(temp["transmitter"]) 123 | test_string = transmitter.fire(transformed) 124 | addr = test_string[189:229] 125 | assert addr[0:19] == "1234 ROADSTREET AVE" 126 | assert addr[19:] == 21*"\x00" 127 | 128 | def test_transmitter_fire_padding_zeros(): 129 | temp = deepcopy(VALID_TRANSMITTER) 130 | temp["transmitter"]["total_number_of_payees"] = "2" 131 | transformed = transmitter.xform(temp["transmitter"]) 132 | test_string = transmitter.fire(transformed) 133 | num_of_payees = test_string[295:303] 134 | assert num_of_payees == "00000002" 135 | 136 | def test_transmitter_fire_blanks_layout(): 137 | temp = deepcopy(VALID_TRANSMITTER) 138 | transformed = transmitter.xform(temp["transmitter"]) 139 | test_string = transmitter.fire(transformed) 140 | for (offset_1_indexed, inclusive_bound) in TRANSMITTER_BLANK_MAP: 141 | yield check_blanks, test_string[(offset_1_indexed -1):inclusive_bound] 142 | 143 | def check_blanks(sub_string): 144 | assert sub_string == len(sub_string)*"\x00" 145 | -------------------------------------------------------------------------------- /spec/test_translator.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | 3 | import os 4 | import re 5 | 6 | from time import gmtime, strftime 7 | 8 | from nose.tools import raises 9 | 10 | from spec_util import check_blanks, \ 11 | PAYER_BLANK_MAP, PAYEE_BLANK_MAP, \ 12 | END_OF_PAYER_BLANK_MAP, END_OF_TRANSMISSION_BLANK_MAP, \ 13 | TRANSMITTER_BLANK_MAP, VALID_ALL_DATA, \ 14 | VALID_ALL_PATH 15 | from fire.translator import translator 16 | 17 | # Tests whether a correct input file generates a correct output file 18 | # Tests whether an output file is defaulted if no path is given 19 | OUTPUT_FILE_PREFIX = "./spec/data/test_outfile" 20 | 21 | def test_translator_run_full_with_specified_output_file(): 22 | output_path = f"{OUTPUT_FILE_PREFIX}_run_valid.ascii" 23 | if os.path.isfile(output_path): 24 | os.remove(output_path) 25 | translator.run(VALID_ALL_PATH, output_path) 26 | assert os.path.isfile(output_path) 27 | with open(output_path, mode='r', encoding='utf-8') as output_file: 28 | ascii_str = output_file.read() 29 | assert len(ascii_str) == 4500 30 | os.remove(output_path) 31 | 32 | def test_translator_run_full_with_default_output_file(): 33 | start_len = len([f for f in os.listdir('./spec/data/') if f.startswith( 34 | "output_{}".format(strftime("%Y-%m-%d", gmtime())))]) 35 | translator.run(VALID_ALL_PATH, None) 36 | file_names = [f for f in os.listdir('./spec/data/') if f.startswith( 37 | "output_{}".format(strftime("%Y-%m-%d", gmtime())))] 38 | assert len(file_names) == start_len + 1 39 | for file in file_names: 40 | if file.startswith("output_"): 41 | os.remove(f"./spec/data/{file}") 42 | 43 | @raises(FileNotFoundError) 44 | def test_translator_run_full_process_invalid_path(): 45 | nonexistant_path = "./spec/data/does/not/exist.json" 46 | if os.path.isfile(nonexistant_path): 47 | os.remove(nonexistant_path) 48 | translator.run(VALID_ALL_PATH, nonexistant_path) 49 | 50 | ######################################################## 51 | 52 | # Tests the load_full_schema method for correct schema structure, presence 53 | # of records, and inserted user data. 54 | # Should fail if no payer/payee data or no transmitter data (key error), or if 55 | # at value from the user data is not present in the returned dict 56 | def test_translator_load_data_into_schema(): 57 | # pylint: disable=invalid-sequence-index 58 | merged_data = translator.load_full_schema(VALID_ALL_DATA) 59 | assert merged_data["transmitter"]["contact_telephone_number_and_ext"] == \ 60 | "5555555555" 61 | assert merged_data["payer"]["payer_tin"] == "123456789" 62 | assert len(merged_data["payees"]) == 2 63 | assert merged_data["end_of_payer"] is not None 64 | assert merged_data["end_of_transmission"] is not None 65 | 66 | # Tests whether sequence numbers start at 1, are sequential (given the specific 67 | # structure of the VALID_ALL_DATA input, and are formatted correctly as 68 | # 8-character strings 69 | def test_translator_insert_sequence_numbers(): 70 | # pylint: disable=invalid-sequence-index 71 | merged_data = translator.load_full_schema(VALID_ALL_DATA) 72 | translator.insert_sequence_numbers(merged_data) 73 | 74 | assert merged_data["transmitter"]["record_sequence_number"] == "00000001" 75 | assert merged_data["payer"]["record_sequence_number"] == "00000002" 76 | assert merged_data["payees"][0]["record_sequence_number"] == "00000003" 77 | assert merged_data["payees"][1]["record_sequence_number"] == "00000004" 78 | assert merged_data["end_of_payer"]["record_sequence_number"] == "00000005" 79 | assert merged_data["end_of_transmission"]["record_sequence_number"] == \ 80 | "00000006" 81 | 82 | 83 | # Tests whether payer and end_of_payer record fields are correctly isnerted 84 | def test_translator_insert_payer_totals(): 85 | # pylint: disable=invalid-sequence-index 86 | data = translator.load_full_schema(VALID_ALL_DATA) 87 | translator.insert_payer_totals(data) 88 | 89 | # Test payer record 90 | assert data["payer"]["amount_codes"] == "123456789ABCDEFG" 91 | assert data["payer"]["number_of_payees"] == "00000002" 92 | 93 | # Test end_of_payer record 94 | # pylint: disable=no-member 95 | values = [v for (k, v) in data["end_of_payer"].items() if \ 96 | re.match(r"^payment_amount_.", k)] 97 | assert len(values) == 16 98 | for v in values: 99 | assert v == "000000000000001700" 100 | 101 | 102 | # Tests whether a transmitter record has the correct total number of payeers/ees 103 | # entered into the "number_of_a_records" and "total_number_of_payees" fields 104 | def test_translator_insert_transmitter_totals(): 105 | # pylint: disable=invalid-sequence-index 106 | data = translator.load_full_schema(VALID_ALL_DATA) 107 | translator.insert_transmitter_totals(data) 108 | 109 | assert data["transmitter"]["total_number_of_payees"] == "00000002" 110 | assert data["end_of_transmission"]["total_number_of_payees"] == "00000002" 111 | assert data["end_of_transmission"]["number_of_a_records"] == "00000001" 112 | 113 | 114 | 115 | # Checks whether a FIRE formatted string has the correct length, blank pos, 116 | # and possible user data in the correct places. Checks that record sequnce 117 | # numbers are in the correct order (using offsets) 118 | def test_translator_get_fire_format(): 119 | data = translator.load_full_schema(VALID_ALL_DATA) 120 | translator.insert_generated_values(data) 121 | ascii_string = translator.get_fire_format(data) 122 | 123 | assert len(ascii_string) == 4500 124 | for (offset, inclusive_bound) in TRANSMITTER_BLANK_MAP: 125 | yield check_blanks, ascii_string[(offset -1):inclusive_bound] 126 | 127 | for (offset, inclusive_bound) in PAYER_BLANK_MAP: 128 | yield check_blanks, ascii_string[(offset + 749):inclusive_bound] 129 | 130 | for (offset, inclusive_bound) in PAYEE_BLANK_MAP: 131 | yield check_blanks, ascii_string[(offset + 749*2):inclusive_bound] 132 | yield check_blanks, ascii_string[(offset + 749*3):inclusive_bound] 133 | 134 | for (offset, inclusive_bound) in END_OF_PAYER_BLANK_MAP: 135 | yield check_blanks, ascii_string[(offset + 749*4):inclusive_bound] 136 | 137 | for (offset, inclusive_bound) in END_OF_TRANSMISSION_BLANK_MAP: 138 | yield check_blanks, ascii_string[(offset + 749*5):inclusive_bound] 139 | -------------------------------------------------------------------------------- /fire/translator/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: Util 3 | Defines a set of classes and functions shared by other modules within 4 | the fire-1099 application. 5 | """ 6 | import re 7 | 8 | # SequenceGenerator: generates sequential integer numbers 9 | class SequenceGenerator: 10 | """ 11 | Generates sequence nubmers via the get_next() method. Not intended for use 12 | in concurrent applications, as increment and string formatting operations 13 | (as well as return) are not executed atomically. 14 | 15 | Attributes 16 | ---------- 17 | self.counter : int 18 | Maintains the last-used sequence number. 19 | 20 | Methods 21 | ---------- 22 | str get_next(): 23 | Increments the sequence number represented by self.counter, and returns 24 | it in the format specified by IRS Publication 1220. 25 | """ 26 | def __init__(self): 27 | self.counter = 0 28 | 29 | def get_next(self): 30 | """ 31 | Returns the next sequence number, formatted as a string according to 32 | IRS Publication 1220. 33 | 34 | Returns 35 | --------- 36 | str 37 | Sequence number. 38 | """ 39 | self.counter += 1 40 | return f"{self.counter:0>8}" 41 | 42 | def get_current(self): 43 | """ 44 | Returns the current sequence number. Does not format or increment. 45 | 46 | Returns 47 | --------- 48 | int 49 | Sequence number. 50 | 51 | """ 52 | return self.counter 53 | 54 | ########## Entity support functions ########## 55 | 56 | def xform_entity(entity_dict, data): 57 | """ 58 | Applies transformation functions specified by the entity dictionary (first 59 | param) to the user-supplied data (second param). If no user data is 60 | supplied for the given field in the entity dictionary, then the default 61 | value for the field is supplied. 62 | 63 | Parameters 64 | ---------- 65 | entity_dict: dict 66 | Dictionary containing all fields required for the type of record 67 | in question. Expected format of this dict is {"key": (value)} where 68 | value is a tuple in the following format: 69 | 70 | (default value, length, fill character, transformation function) 71 | 72 | data: dict 73 | Data to be transformed and inserted into the returned dict. 74 | All keys in this dict are expected to be present in entity_dict. 75 | 76 | Returns 77 | ---------- 78 | dict 79 | Dictionary containing all fields specified in parameter "entity_dict", 80 | with transformed values from parameter "data" or defaults. 81 | 82 | """ 83 | data_dict = {} 84 | for key, (default, _, _, transform) in entity_dict.items(): 85 | if key in data: 86 | data_dict[key] = transform(data[key]) 87 | else: 88 | data_dict[key] = default 89 | return data_dict 90 | 91 | def fire_entity(entity_dict, key_ordering, data, expected_length=750): 92 | """ 93 | Applies transformation functions specified by the entity dictionary (first 94 | param) to the user-supplied data (second param). If no user data is 95 | supplied for the given field in the entity dictionary, then the default 96 | value for the field is supplied. 97 | 98 | Parameters 99 | ---------- 100 | entity_dict: dict 101 | Dictionary containing all fields required for the type of record 102 | in question. Expected format of this dict is {"key": (value)} where 103 | value is a tuple in the following format: 104 | 105 | (default value, length, fill character, transformation function) 106 | 107 | data: dict 108 | Data to be transformed and inserted into the returned dict. 109 | All keys in this dict are expected to be present in entity_dict. 110 | 111 | Returns 112 | ---------- 113 | dict 114 | Dictionary containing all fields specified in parameter "entity_dict", 115 | with transformed values from parameter "data" or defaults. 116 | 117 | """ 118 | record_string = "" 119 | for key in key_ordering: 120 | _, length, fill_char, _ = entity_dict[key] 121 | new_string = data[key].ljust(length, fill_char) 122 | record_string += new_string 123 | if len(new_string) != length: 124 | raise Exception(f"Generated a record string of incorrect length: \ 125 | Expected: {length} -- Actual: {len(new_string)} \ 126 | -- Key: {key} -- Value: {record_string}") 127 | if len(record_string) != expected_length: 128 | raise Exception(f"Generated records string of invalid length: \ 129 | {len(record_string)}") 130 | return record_string 131 | 132 | """ 133 | Transformations on user-supplied data 134 | ------------------------------------- 135 | Many attributes in each record / entity type share similar 136 | formatting requirements according to IRS Pub 1220. For example, most text 137 | fields are required to contain uppercase characters. 138 | 139 | The functions below facilitate transformations that are similar across 140 | different fields and entities. 141 | """ 142 | 143 | 144 | def digits_only(value): 145 | """ 146 | Removes all non-digit characters 147 | """ 148 | return re.sub("[^0-9]*", "", value) 149 | 150 | def uppercase(value): 151 | """ 152 | Returns the string with all alpha characters in uppercase 153 | """ 154 | return value.upper() 155 | 156 | def rjust_zero(value, length): 157 | """ 158 | right-justifies *value* and pads with zeros to *length* 159 | """ 160 | return f"{digits_only(value):0>{length}}" 161 | 162 | def factor_transforms(transforms): 163 | """ 164 | Factor a list of transform tuples into a list of sort keys and a dict of 165 | transforms 166 | 167 | Parameters 168 | ---------- 169 | transforms : list of tuple 170 | The transforms to factor 171 | 172 | Returns 173 | ------- 174 | sort_keys : list of str 175 | The names of the transforms, in their original order 176 | transform_dict : dict of tuple 177 | A dict of the transforms, keyed by their sort key 178 | """ 179 | sort_keys = list() 180 | transform_dict = dict() 181 | 182 | for transform_name, transform in transforms: 183 | sort_keys.append(transform_name) 184 | transform_dict[transform_name] = transform 185 | 186 | return sort_keys, transform_dict 187 | -------------------------------------------------------------------------------- /fire/schema/1099_NEC_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "A schema for validating the new 1099 NEC data formatted according to IRS Publication 1220.", 4 | "type": "object", 5 | "properties":{ 6 | "transmitter":{ 7 | "type": "object", 8 | "properties": { 9 | "record_type": {"type": "string", "maxLength": 1}, 10 | "payment_year": {"$ref": "#/definitions/year"}, 11 | "prior_year_data_indicator": {"type": "string", "maxLength": 1}, 12 | "transmitter_tin": {"$ref": "#/definitions/tin"}, 13 | "transmitter_control_code": {"$ref": "#/definitions/transmitter_control_code"}, 14 | "test_file_indicator": {"type": "string", "maxLength": 1}, 15 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 16 | "transmitter_name": {"$ref": "#/definitions/generic_name"}, 17 | "transmitter_name_contd": {"$ref": "#/definitions/generic_name"}, 18 | "company_name": {"$ref": "#/definitions/generic_name"}, 19 | "company_name_contd": {"$ref": "#/definitions/generic_name"}, 20 | "company_mailing_address": {"$ref": "#/definitions/address"}, 21 | "company_city": {"$ref": "#/definitions/city"}, 22 | "company_state": {"$ref": "#/definitions/state"}, 23 | "company_zip_code": {"$ref": "#/definitions/zip_code"}, 24 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 25 | "contact_name": {"$ref": "#/definitions/generic_name"}, 26 | "contact_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 27 | "contact_email_address": {"$ref": "#/definitions/email"}, 28 | "record_sequence_number": {"type": "string", "maxLength": 8}, 29 | "vendor_indicator": {"type": "string", "maxLength": 1}, 30 | "vendor_name": {"$ref": "#/definitions/generic_name"}, 31 | "vendor_mailing_address": {"$ref": "#/definitions/address"}, 32 | "vendor_city": {"$ref": "#/definitions/city"}, 33 | "vendor_state": {"$ref": "#/definitions/state"}, 34 | "vendor_zip_code": {"$ref": "#/definitions/zip_code"}, 35 | "vendor_contact_name": {"$ref": "#/definitions/generic_name"}, 36 | "vendor_contact_telephone_and_ext": {"$ref": "#/definitions/phone"}, 37 | "vendor_foreign_entity_indicator": {"type": "string", "maxLength": 1} 38 | }, 39 | "required":["transmitter_name", "company_name", "company_mailing_address", 40 | "company_city", "company_state", "company_zip_code", "transmitter_tin", 41 | "transmitter_control_code", "contact_name", 42 | "contact_telephone_number_and_ext", "contact_email_address", 43 | "payment_year"] 44 | }, 45 | "payer":{ 46 | "type:":"object", 47 | "properties":{ 48 | "record_type": {"type": "string", "maxLength": 1}, 49 | "payment_year": {"$ref": "#/definitions/year"}, 50 | "combined_fed_state": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 51 | "payer_tin": {"$ref": "#/definitions/tin"}, 52 | "payer_name_control": {"type": "string", "maxLength": 4}, 53 | "last_filing_indicator": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 54 | "type_of_return": {"type": "string", "maxLength": 2}, 55 | "amount_codes": {"type": "string", "maxLength": 16}, 56 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 57 | "first_payer_name": {"$ref": "#/definitions/generic_name"}, 58 | "second_payer_name": {"$ref": "#/definitions/generic_name"}, 59 | "transfer_agent_control": {"type": "string", "maxLength": 1}, 60 | "payer_shipping_address": {"$ref": "#/definitions/address"}, 61 | "payer_city": {"$ref": "#/definitions/city"}, 62 | "payer_state": {"$ref": "#/definitions/state"}, 63 | "payer_zip_code": {"$ref": "#/definitions/zip_code"}, 64 | "payer_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 65 | "record_sequence_number": {"type": "string", "maxLength": 8} 66 | }, 67 | "required":[ 68 | "first_payer_name", "payment_year", "payer_shipping_address", "payer_city", 69 | "payer_state", "payer_zip_code", "payer_tin", "payer_name_control", 70 | "payer_telephone_number_and_ext" 71 | ] 72 | }, 73 | "payees":{ 74 | "type":"array", 75 | "items":{ 76 | "type": "object", 77 | "properties": { 78 | "record_type": {"type": "string", "maxLength": 1}, 79 | "payment_year": {"$ref": "#/definitions/year"}, 80 | "corrected_return_indicator": {"type": "string", "maxLength": 1}, 81 | "payees_name_control": {"type": "string", "maxLength": 4}, 82 | "type_of_tin": {"type": "string", "maxLength": 1}, 83 | "payees_tin": {"$ref": "#/definitions/tin"}, 84 | "payers_account_number_for_payee": {"type": "string", "maxLength": 20}, 85 | "payers_office_code": {"type": "string", "maxLength": 4}, 86 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 87 | "foreign_country_indicator": {"type": "string", "maxLength": 1}, 88 | "first_payee_name_line": {"$ref": "#/definitions/generic_name"}, 89 | "second_payee_name_line": {"$ref": "#/definitions/generic_name"}, 90 | "payee_mailing_address": {"$ref": "#/definitions/address"}, 91 | "payee_city": {"$ref": "#/definitions/city"}, 92 | "payee_state": {"$ref": "#/definitions/state"}, 93 | "payee_zip_code": {"$ref": "#/definitions/zip_code"}, 94 | "record_sequence_number": {"type": "string", "maxLength": 8}, 95 | "second_tin_notice": {"type": "string", "maxLength": 1}, 96 | "direct_sales_indicator": {"type": "string", "maxLength": 1}, 97 | "state_income_tax_withheld": {"type": "string", "maxLength": 12}, 98 | "local_income_tax_withheld": {"type": "string", "maxLength": 12}, 99 | "combined_federal_state_code": {"type": "string", "maxLength": 2} 100 | }, 101 | "required":[ 102 | "first_payee_name_line", "payees_name_control", "payment_year", 103 | "payee_mailing_address", "payee_city", "payee_state", 104 | "payee_zip_code", "payees_tin", "payment_amount_1" 105 | ] 106 | } 107 | }, 108 | "end_of_payer":{ 109 | "type": "object", 110 | "properties":{ 111 | "record_type": {"type": "string", "maxLength": 1}, 112 | "number_of_payees": {"type": "string", "maxLength": 8}, 113 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 114 | "record_sequence_number": {"type": "string", "maxLength": 8} 115 | } 116 | }, 117 | "end_of_transmission":{ 118 | "type": "object", 119 | "properties":{ 120 | "record_type": {"type": "string", "maxLength": 1}, 121 | "number_of_a_records": {"type": "string", "maxLength": 8}, 122 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 123 | "record_sequence_number": {"type": "string", "maxLength": 8} 124 | } 125 | } 126 | }, 127 | "definitions":{ 128 | "tin":{ 129 | "type":"string", 130 | "pattern":"(^[0-9]{2}[ -]?[0-9]{7}$)|(^[0-9]{3}[ -]?[0-9]{2}[ -]?[0-9]{4}$)" 131 | }, 132 | "state":{ 133 | "type":"string", 134 | "pattern":"^[a-zA-Z]{2}$" 135 | }, 136 | "zip_code":{ 137 | "type:":"string", 138 | "pattern":"^[0-9]{5}(-[0-9]{4})?$" 139 | }, 140 | "email":{ 141 | "type":"string", 142 | "maxLength": 50, 143 | "pattern": "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" 144 | }, 145 | "address":{ 146 | "type": "string", 147 | "maxLength": 40 148 | }, 149 | "city":{ 150 | "type": "string", 151 | "maxLength": 40 152 | }, 153 | "phone":{ 154 | "type": "string", 155 | "pattern": "^\\(?[0-9]{3}\\)?[ .-]?[0-9]{3}[ .-]?[0-9]{4}$" 156 | }, 157 | "year":{ 158 | "type": "string", 159 | "pattern":"^[0-9]{4}", 160 | "maxLength": 4 161 | }, 162 | "generic_name":{ 163 | "type": "string", 164 | "maxLength": 40 165 | }, 166 | "name_control":{ 167 | "type": "string", 168 | "minLength": 4, 169 | "maxLength": 4 170 | }, 171 | "dollar_amount":{ 172 | "type": "string", 173 | "pattern": "^[\\$]?[0-9,]*\\.?[0-9]{2}$" 174 | }, 175 | "foreign_entity": { 176 | "type": "string", 177 | "pattern": "^1?$" 178 | }, 179 | "transmitter_control_code": { 180 | "type": "string", 181 | "pattern": "^[a-zA-Z0-9]{5}$", 182 | "maxLength": 5 183 | } 184 | }, 185 | "additional_properties": false 186 | } 187 | -------------------------------------------------------------------------------- /fire/translator/translator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module: Translator 3 | Processes user-provided JSON file into an output file in the format 4 | required by IRS Publication 1220. 5 | 6 | Support notes: 7 | * 1099-MISC and 1099-NEC files only. 8 | * Singly payer only. For multiple payers, use multiple input files. 9 | """ 10 | import os.path 11 | import json 12 | from time import gmtime, strftime 13 | from jsonschema import validate 14 | import click 15 | 16 | from fire.entities import transmitter, payer, payees, end_of_payer, end_of_transmission 17 | from .util import SequenceGenerator 18 | 19 | 20 | @click.command() 21 | @click.argument("input_path", type=click.Path(exists=True)) 22 | @click.option( 23 | "--output", type=click.Path(), help="system path for the output to be generated" 24 | ) 25 | @click.option("--type", "-t", help="NEC or MISC") 26 | def cli(input_path, output, type="MISC"): 27 | """ 28 | Convert a JSON input file into the format required by IRS Publication 1220 29 | 30 | \b 31 | input_path: system path for file containing the user input JSON data 32 | """ 33 | run(input_path, output, type) 34 | 35 | 36 | def run(input_path, output_path, type="MISC"): 37 | """ 38 | Sequentially calls helper functions to fully process : 39 | * Load user JSON data from input file 40 | * Transform user data and merge into a master schema 41 | * Generate and insert computed values into master 42 | * Format ASCII string representing user- and system-generated data 43 | * Write ASCII string to output file 44 | 45 | Parameters 46 | ---------- 47 | input_path : str 48 | system path for file containing the user input JSON data 49 | output : str 50 | optional system path for the output to be generated 51 | 52 | """ 53 | module_path = os.path.split(os.path.realpath(__file__))[0] 54 | schema_path = os.path.join( 55 | module_path, 56 | "../schema", 57 | "1099_MISC_schema.json" if type == "MISC" else "1099_NEC_schema.json", 58 | ) 59 | input_dirname = os.path.dirname(os.path.abspath(input_path)) 60 | if output_path is None: 61 | output_path = "{}/output_{}".format( 62 | input_dirname, strftime("%Y-%m-%d %H_%M_%S", gmtime()) 63 | ) 64 | 65 | user_data = extract_user_data(input_path) 66 | validate_user_data(user_data, schema_path) 67 | 68 | master = load_full_schema(user_data) 69 | insert_generated_values(master) 70 | 71 | ascii_string = get_fire_format(master) 72 | write_1099_file(ascii_string, output_path) 73 | 74 | 75 | def extract_user_data(path): 76 | """ 77 | Opens file at path specified by input parameter. Reads data as JSON and 78 | returns a dict containing that JSON data. 79 | 80 | Parameters 81 | ---------- 82 | path : str 83 | system path for file containing the user input JSON data 84 | 85 | Returns 86 | ---------- 87 | dict 88 | JSON data loaded from file at input path 89 | """ 90 | user_data = {} 91 | with open(path, mode="r", encoding="utf-8") as file: 92 | user_data = json.load(file) 93 | return user_data 94 | 95 | 96 | def validate_user_data(data, schema_path): 97 | """ 98 | Validates data (first param) against the base schema (second param) 99 | 100 | Parameters 101 | ---------- 102 | data : dict 103 | data to be validated 104 | 105 | schema_path: str 106 | system path for file containing schema to data validate against 107 | 108 | """ 109 | with open(schema_path, mode="r", encoding="utf-8") as schema: 110 | schema = json.load(schema) 111 | validate(data, schema) 112 | 113 | 114 | def load_full_schema(data): 115 | """ 116 | Merges data into the master schema for records, including fields that were 117 | not specified in the data originally loaded (such as system-generated fields 118 | and optional fields). 119 | 120 | Parameters 121 | ---------- 122 | data : dict 123 | JSON data to be merged into master schema 124 | 125 | Returns 126 | ---------- 127 | dict 128 | Master schema with all fields provided in input parameter included 129 | 130 | """ 131 | merged_data = { 132 | "transmitter": "", 133 | "payer": "", 134 | "payees": [], 135 | "end_of_payer": "", 136 | "end_of_transmission": "", 137 | } 138 | merged_data["transmitter"] = transmitter.xform(data["transmitter"]) 139 | merged_data["payer"] = payer.xform(data["payer"]) 140 | merged_data["payees"] = payees.xform(data["payees"]) 141 | merged_data["end_of_payer"] = end_of_payer.xform({}) 142 | merged_data["end_of_transmission"] = end_of_transmission.xform({}) 143 | 144 | return merged_data 145 | 146 | 147 | def insert_generated_values(data): 148 | """ 149 | Inserts system-generated values into the appropriate fields. _Note: this 150 | edits the dict object provided as a parameter in-place._ 151 | 152 | Examples of fields inserted: [all]::record_sequence_number, 153 | payer::number_of_payees, transmitter::total_number_of_payees, etc. 154 | 155 | Parameters 156 | ---------- 157 | data : dict 158 | Dictionary containing "master" set of records. It is expected that 159 | this includes end_of_payer and end_of_transmission records, with all 160 | fields captured. 161 | 162 | """ 163 | insert_sequence_numbers(data) 164 | insert_payer_totals(data) 165 | insert_transmitter_totals(data) 166 | 167 | 168 | def insert_sequence_numbers(data): 169 | """ 170 | Inserts sequence numbers into each record, in the following order: 171 | transmitter, payer, payee(s) (each in order supplied by user), 172 | end of payer, end of transmission. 173 | 174 | _Note: this edits the input parameter in-place._ 175 | 176 | Parameters 177 | ---------- 178 | data : dict 179 | Dictionary into which sequence numbers will be inserted. 180 | 181 | """ 182 | seq = SequenceGenerator() 183 | 184 | # Warning: order of below statements is important; do not re-arrange 185 | data["transmitter"]["record_sequence_number"] = seq.get_next() 186 | data["payer"]["record_sequence_number"] = seq.get_next() 187 | for payee in data["payees"]: 188 | payee["record_sequence_number"] = seq.get_next() 189 | data["end_of_payer"]["record_sequence_number"] = seq.get_next() 190 | data["end_of_transmission"]["record_sequence_number"] = seq.get_next() 191 | 192 | 193 | def insert_payer_totals(data): 194 | """ 195 | Inserts requried values into the payer and end_of_payer records. This 196 | includes values for the following fields: payment_amount_*, 197 | amount_codes, number_of_payees, total_number_of_payees, number_of_a_records. 198 | 199 | _Note: this edits the input parameter in-place._ 200 | 201 | Parameters 202 | ---------- 203 | data : dict 204 | Dictionary containing payer, payee, and end_of_payer records, into which 205 | computed values will be inserted. 206 | 207 | """ 208 | codes = [ 209 | "1", 210 | "2", 211 | "3", 212 | "4", 213 | "5", 214 | "6", 215 | "7", 216 | "8", 217 | "9", 218 | "A", 219 | "B", 220 | "C", 221 | "D", 222 | "E", 223 | "F", 224 | "G", 225 | "H", 226 | "J" 227 | ] 228 | totals = [0 for _ in range(len(codes))] 229 | payer_code_string = "" 230 | 231 | for payee in data["payees"]: 232 | for i, code in enumerate(codes): 233 | try: 234 | totals[i] += int(payee["payment_amount_" + code]) 235 | except ValueError: 236 | pass 237 | 238 | for i, (total, code) in enumerate(zip(totals, codes)): 239 | if total != 0: 240 | payer_code_string += code 241 | data["end_of_payer"]["payment_amount_" + code] = f"{total:0>18}" 242 | 243 | data["payer"]["amount_codes"] = str(payer_code_string) 244 | payee_count = len(data["payees"]) 245 | data["payer"]["number_of_payees"] = f"{payee_count:0>8}" 246 | data["end_of_payer"]["number_of_payees"] = f"{payee_count:0>8}" 247 | 248 | 249 | def insert_transmitter_totals(data): 250 | """ 251 | Inserts requried values into the transmitter and end_of_transmission 252 | records. This includes values for the following fields: 253 | total_number_of_payees, number_of_a_records. 254 | 255 | _Note: this edits the input parameter in-place._ 256 | 257 | Parameters 258 | ---------- 259 | data : dict 260 | Dictionary containing transmitter and end_of_transmission records, 261 | into which computed values will be inserted. 262 | 263 | """ 264 | payee_count = len(data["payees"]) 265 | data["transmitter"]["total_number_of_payees"] = f"{payee_count:0>8}" 266 | data["end_of_transmission"]["total_number_of_payees"] = f"{payee_count:0>8}" 267 | # Force number of A records to "1" as only one payer is supported 268 | data["end_of_transmission"]["number_of_a_records"] = "00000001" 269 | 270 | 271 | def get_fire_format(data): 272 | """ 273 | Returns the input dictionary converted into the string format required by 274 | the IRS FIRE electronic filing system. It is expceted that the input 275 | dictionary has the following correctly formatted items: 276 | * transmitter (dict) 277 | * payer (dict) 278 | * payees (array of dict objects) 279 | * end_of_payer (dict) 280 | * end_of_transmission 281 | 282 | Parameters 283 | ---------- 284 | data : dict 285 | Dictionary containing records to be processed into a FIRE-formatted 286 | string. 287 | 288 | Returns 289 | ---------- 290 | str 291 | FIRE-formatted string containing data provided as the input parameter. 292 | 293 | """ 294 | fire_string = "" 295 | 296 | fire_string += transmitter.fire(data["transmitter"]) 297 | fire_string += payer.fire(data["payer"]) 298 | fire_string += payees.fire(data["payees"]) 299 | fire_string += end_of_payer.fire(data["end_of_payer"]) 300 | fire_string += end_of_transmission.fire(data["end_of_transmission"]) 301 | 302 | return fire_string 303 | 304 | 305 | def write_1099_file(formatted_string, path): 306 | """ 307 | Writes the given string to a file at the given path. If the file does not 308 | exist, it will be created. 309 | 310 | Parameters 311 | ---------- 312 | formatted_string : str 313 | FIRE-formatted string to be written to disk. 314 | 315 | path: str 316 | Path of file to be written. 317 | 318 | """ 319 | file = open(path, mode="w+") 320 | file.write(formatted_string) 321 | file.close() 322 | -------------------------------------------------------------------------------- /fire/schema/base_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "A schema for validating 1099 MISC data formatted according to IRS Publication 1220.", 4 | "type": "object", 5 | "properties":{ 6 | "transmitter":{ 7 | "type": "object", 8 | "properties": { 9 | "record_type": {"type": "string", "maxLength": 1}, 10 | "payment_year": {"$ref": "#/definitions/year"}, 11 | "prior_year_data_indicator": {"type": "string", "maxLength": 1}, 12 | "transmitter_tin": {"$ref": "#/definitions/tin"}, 13 | "transmitter_control_code": {"$ref": "#/definitions/transmitter_control_code"}, 14 | "test_file_indicator": {"type": "string", "maxLength": 1}, 15 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 16 | "transmitter_name": {"$ref": "#/definitions/generic_name"}, 17 | "transmitter_name_contd": {"$ref": "#/definitions/generic_name"}, 18 | "company_name": {"$ref": "#/definitions/generic_name"}, 19 | "company_name_contd": {"$ref": "#/definitions/generic_name"}, 20 | "company_mailing_address": {"$ref": "#/definitions/address"}, 21 | "company_city": {"$ref": "#/definitions/city"}, 22 | "company_state": {"$ref": "#/definitions/state"}, 23 | "company_zip_code": {"$ref": "#/definitions/zip_code"}, 24 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 25 | "contact_name": {"$ref": "#/definitions/generic_name"}, 26 | "contact_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 27 | "contact_email_address": {"$ref": "#/definitions/email"}, 28 | "record_sequence_number": {"type": "string", "maxLength": 8}, 29 | "vendor_indicator": {"type": "string", "maxLength": 1}, 30 | "vendor_name": {"$ref": "#/definitions/generic_name"}, 31 | "vendor_mailing_address": {"$ref": "#/definitions/address"}, 32 | "vendor_city": {"$ref": "#/definitions/city"}, 33 | "vendor_state": {"$ref": "#/definitions/state"}, 34 | "vendor_zip_code": {"$ref": "#/definitions/zip_code"}, 35 | "vendor_contact_name": {"$ref": "#/definitions/generic_name"}, 36 | "vendor_contact_telephone_and_ext": {"$ref": "#/definitions/phone"}, 37 | "vendor_foreign_entity_indicator": {"type": "string", "maxLength": 1} 38 | }, 39 | "required":["transmitter_name", "company_name", "company_mailing_address", 40 | "company_city", "company_state", "company_zip_code", "transmitter_tin", 41 | "transmitter_control_code", "contact_name", 42 | "contact_telephone_number_and_ext", "contact_email_address", 43 | "payment_year"] 44 | }, 45 | "payer":{ 46 | "type:":"object", 47 | "properties":{ 48 | "record_type": {"type": "string", "maxLength": 1}, 49 | "payment_year": {"$ref": "#/definitions/year"}, 50 | "combined_fed_state": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 51 | "payer_tin": {"$ref": "#/definitions/tin"}, 52 | "payer_name_control": {"type": "string", "maxLength": 4}, 53 | "last_filing_indicator": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 54 | "type_of_return": {"type": "string", "maxLength": 2}, 55 | "amount_codes": {"type": "string", "maxLength": 16}, 56 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 57 | "first_payer_name": {"$ref": "#/definitions/generic_name"}, 58 | "second_payer_name": {"$ref": "#/definitions/generic_name"}, 59 | "transfer_agent_control": {"type": "string", "maxLength": 1}, 60 | "payer_shipping_address": {"$ref": "#/definitions/address"}, 61 | "payer_city": {"$ref": "#/definitions/city"}, 62 | "payer_state": {"$ref": "#/definitions/state"}, 63 | "payer_zip_code": {"$ref": "#/definitions/zip_code"}, 64 | "payer_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 65 | "record_sequence_number": {"type": "string", "maxLength": 8} 66 | }, 67 | "required":[ 68 | "first_payer_name", "payment_year", "payer_shipping_address", "payer_city", 69 | "payer_state", "payer_zip_code", "payer_tin", "payer_name_control", 70 | "payer_telephone_number_and_ext" 71 | ] 72 | }, 73 | "payees":{ 74 | "type":"array", 75 | "items":{ 76 | "type": "object", 77 | "properties": { 78 | "record_type": {"type": "string", "maxLength": 1}, 79 | "payment_year": {"$ref": "#/definitions/year"}, 80 | "corrected_return_indicator": {"type": "string", "maxLength": 1}, 81 | "payees_name_control": {"type": "string", "maxLength": 4}, 82 | "type_of_tin": {"type": "string", "maxLength": 1}, 83 | "payees_tin": {"$ref": "#/definitions/tin"}, 84 | "payers_account_number_for_payee": {"type": "string", "maxLength": 20}, 85 | "payers_office_code": {"type": "string", "maxLength": 4}, 86 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 87 | "payment_amount_2": {"$ref": "#/definitions/dollar_amount"}, 88 | "payment_amount_3": {"$ref": "#/definitions/dollar_amount"}, 89 | "payment_amount_4": {"$ref": "#/definitions/dollar_amount"}, 90 | "payment_amount_5": {"$ref": "#/definitions/dollar_amount"}, 91 | "payment_amount_6": {"$ref": "#/definitions/dollar_amount"}, 92 | "payment_amount_7": {"$ref": "#/definitions/dollar_amount"}, 93 | "payment_amount_8": {"$ref": "#/definitions/dollar_amount"}, 94 | "payment_amount_9": {"$ref": "#/definitions/dollar_amount"}, 95 | "payment_amount_A": {"$ref": "#/definitions/dollar_amount"}, 96 | "payment_amount_B": {"$ref": "#/definitions/dollar_amount"}, 97 | "payment_amount_C": {"$ref": "#/definitions/dollar_amount"}, 98 | "payment_amount_D": {"$ref": "#/definitions/dollar_amount"}, 99 | "payment_amount_E": {"$ref": "#/definitions/dollar_amount"}, 100 | "payment_amount_F": {"$ref": "#/definitions/dollar_amount"}, 101 | "payment_amount_G": {"$ref": "#/definitions/dollar_amount"}, 102 | "foreign_country_indicator": {"type": "string", "maxLength": 1}, 103 | "first_payee_name_line": {"$ref": "#/definitions/generic_name"}, 104 | "second_payee_name_line": {"$ref": "#/definitions/generic_name"}, 105 | "payee_mailing_address": {"$ref": "#/definitions/address"}, 106 | "payee_city": {"$ref": "#/definitions/city"}, 107 | "payee_state": {"$ref": "#/definitions/state"}, 108 | "payee_zip_code": {"$ref": "#/definitions/zip_code"}, 109 | "record_sequence_number": {"type": "string", "maxLength": 8}, 110 | "second_tin_notice": {"type": "string", "maxLength": 1}, 111 | "direct_sales_indicator": {"type": "string", "maxLength": 1}, 112 | "fatca_filing_requirement_indicator": {"type": "string", "maxLength": 1}, 113 | "special_data_entries": {"type": "string", "maxLength": 60}, 114 | "state_income_tax_withheld": {"type": "string", "maxLength": 12}, 115 | "local_income_tax_withheld": {"type": "string", "maxLength": 12}, 116 | "combined_federal_state_code": {"type": "string", "maxLength": 2} 117 | }, 118 | "required":[ 119 | "first_payee_name_line", "payees_name_control", "payment_year", 120 | "payee_mailing_address", "payee_city", "payee_state", 121 | "payee_zip_code", "payees_tin", "payment_amount_7" 122 | ] 123 | } 124 | }, 125 | "end_of_payer":{ 126 | "type": "object", 127 | "properties":{ 128 | "record_type": {"type": "string", "maxLength": 1}, 129 | "number_of_payees": {"type": "string", "maxLength": 8}, 130 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 131 | "payment_amount_2": {"$ref": "#/definitions/dollar_amount"}, 132 | "payment_amount_3": {"$ref": "#/definitions/dollar_amount"}, 133 | "payment_amount_4": {"$ref": "#/definitions/dollar_amount"}, 134 | "payment_amount_5": {"$ref": "#/definitions/dollar_amount"}, 135 | "payment_amount_6": {"$ref": "#/definitions/dollar_amount"}, 136 | "payment_amount_7": {"$ref": "#/definitions/dollar_amount"}, 137 | "payment_amount_8": {"$ref": "#/definitions/dollar_amount"}, 138 | "payment_amount_9": {"$ref": "#/definitions/dollar_amount"}, 139 | "payment_amount_A": {"$ref": "#/definitions/dollar_amount"}, 140 | "payment_amount_B": {"$ref": "#/definitions/dollar_amount"}, 141 | "payment_amount_C": {"$ref": "#/definitions/dollar_amount"}, 142 | "payment_amount_D": {"$ref": "#/definitions/dollar_amount"}, 143 | "payment_amount_E": {"$ref": "#/definitions/dollar_amount"}, 144 | "payment_amount_F": {"$ref": "#/definitions/dollar_amount"}, 145 | "payment_amount_G": {"$ref": "#/definitions/dollar_amount"}, 146 | "record_sequence_number": {"type": "string", "maxLength": 8} 147 | } 148 | }, 149 | "end_of_transmission":{ 150 | "type": "object", 151 | "properties":{ 152 | "record_type": {"type": "string", "maxLength": 1}, 153 | "number_of_a_records": {"type": "string", "maxLength": 8}, 154 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 155 | "record_sequence_number": {"type": "string", "maxLength": 8} 156 | } 157 | } 158 | }, 159 | "definitions":{ 160 | "tin":{ 161 | "type":"string", 162 | "pattern":"(^[0-9]{2}[ -]?[0-9]{7}$)|(^[0-9]{3}[ -]?[0-9]{2}[ -]?[0-9]{4}$)" 163 | }, 164 | "state":{ 165 | "type":"string", 166 | "pattern":"^[a-zA-Z]{2}$" 167 | }, 168 | "zip_code":{ 169 | "type:":"string", 170 | "pattern":"^[0-9]{5}(-[0-9]{4})?$" 171 | }, 172 | "email":{ 173 | "type":"string", 174 | "maxLength": 50, 175 | "pattern": "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" 176 | }, 177 | "address":{ 178 | "type": "string", 179 | "maxLength": 40 180 | }, 181 | "city":{ 182 | "type": "string", 183 | "maxLength": 40 184 | }, 185 | "phone":{ 186 | "type": "string", 187 | "pattern": "^\\(?[0-9]{3}\\)?[ .-]?[0-9]{3}[ .-]?[0-9]{4}$" 188 | }, 189 | "year":{ 190 | "type": "string", 191 | "pattern":"^[0-9]{4}", 192 | "maxLength": 4 193 | }, 194 | "generic_name":{ 195 | "type": "string", 196 | "maxLength": 40 197 | }, 198 | "name_control":{ 199 | "type": "string", 200 | "minLength": 4, 201 | "maxLength": 4 202 | }, 203 | "dollar_amount":{ 204 | "type": "string", 205 | "pattern": "^[\\$]?[0-9,]*\\.?[0-9]{2}$" 206 | }, 207 | "foreign_entity": { 208 | "type": "string", 209 | "pattern": "^1?$" 210 | }, 211 | "transmitter_control_code": { 212 | "type": "string", 213 | "pattern": "^[a-zA-Z0-9]{5}$", 214 | "maxLength": 5 215 | } 216 | }, 217 | "additional_properties": false 218 | } 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /fire/schema/1099_MISC_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "A schema for validating 1099 MISC data formatted according to IRS Publication 1220.", 4 | "type": "object", 5 | "properties":{ 6 | "transmitter":{ 7 | "type": "object", 8 | "properties": { 9 | "record_type": {"type": "string", "maxLength": 1}, 10 | "payment_year": {"$ref": "#/definitions/year"}, 11 | "prior_year_data_indicator": {"type": "string", "maxLength": 1}, 12 | "transmitter_tin": {"$ref": "#/definitions/tin"}, 13 | "transmitter_control_code": {"$ref": "#/definitions/transmitter_control_code"}, 14 | "test_file_indicator": {"type": "string", "maxLength": 1}, 15 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 16 | "transmitter_name": {"$ref": "#/definitions/generic_name"}, 17 | "transmitter_name_contd": {"$ref": "#/definitions/generic_name"}, 18 | "company_name": {"$ref": "#/definitions/generic_name"}, 19 | "company_name_contd": {"$ref": "#/definitions/generic_name"}, 20 | "company_mailing_address": {"$ref": "#/definitions/address"}, 21 | "company_city": {"$ref": "#/definitions/city"}, 22 | "company_state": {"$ref": "#/definitions/state"}, 23 | "company_zip_code": {"$ref": "#/definitions/zip_code"}, 24 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 25 | "contact_name": {"$ref": "#/definitions/generic_name"}, 26 | "contact_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 27 | "contact_email_address": {"$ref": "#/definitions/email"}, 28 | "record_sequence_number": {"type": "string", "maxLength": 8}, 29 | "vendor_indicator": {"type": "string", "maxLength": 1}, 30 | "vendor_name": {"$ref": "#/definitions/generic_name"}, 31 | "vendor_mailing_address": {"$ref": "#/definitions/address"}, 32 | "vendor_city": {"$ref": "#/definitions/city"}, 33 | "vendor_state": {"$ref": "#/definitions/state"}, 34 | "vendor_zip_code": {"$ref": "#/definitions/zip_code"}, 35 | "vendor_contact_name": {"$ref": "#/definitions/generic_name"}, 36 | "vendor_contact_telephone_and_ext": {"$ref": "#/definitions/phone"}, 37 | "vendor_foreign_entity_indicator": {"type": "string", "maxLength": 1} 38 | }, 39 | "required":["transmitter_name", "company_name", "company_mailing_address", 40 | "company_city", "company_state", "company_zip_code", "transmitter_tin", 41 | "transmitter_control_code", "contact_name", 42 | "contact_telephone_number_and_ext", "contact_email_address", 43 | "payment_year"] 44 | }, 45 | "payer":{ 46 | "type:":"object", 47 | "properties":{ 48 | "record_type": {"type": "string", "maxLength": 1}, 49 | "payment_year": {"$ref": "#/definitions/year"}, 50 | "combined_fed_state": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 51 | "payer_tin": {"$ref": "#/definitions/tin"}, 52 | "payer_name_control": {"type": "string", "maxLength": 4}, 53 | "last_filing_indicator": {"type": "string", "maxLength": 1, "pattern": "^[1]?$"}, 54 | "type_of_return": {"type": "string", "maxLength": 2}, 55 | "amount_codes": {"type": "string", "maxLength": 16}, 56 | "foreign_entity_indicator": {"type": "string", "maxLength": 1}, 57 | "first_payer_name": {"$ref": "#/definitions/generic_name"}, 58 | "second_payer_name": {"$ref": "#/definitions/generic_name"}, 59 | "transfer_agent_control": {"type": "string", "maxLength": 1}, 60 | "payer_shipping_address": {"$ref": "#/definitions/address"}, 61 | "payer_city": {"$ref": "#/definitions/city"}, 62 | "payer_state": {"$ref": "#/definitions/state"}, 63 | "payer_zip_code": {"$ref": "#/definitions/zip_code"}, 64 | "payer_telephone_number_and_ext": {"$ref": "#/definitions/phone"}, 65 | "record_sequence_number": {"type": "string", "maxLength": 8} 66 | }, 67 | "required":[ 68 | "first_payer_name", "payment_year", "payer_shipping_address", "payer_city", 69 | "payer_state", "payer_zip_code", "payer_tin", "payer_name_control", 70 | "payer_telephone_number_and_ext" 71 | ] 72 | }, 73 | "payees":{ 74 | "type":"array", 75 | "items":{ 76 | "type": "object", 77 | "properties": { 78 | "record_type": {"type": "string", "maxLength": 1}, 79 | "payment_year": {"$ref": "#/definitions/year"}, 80 | "corrected_return_indicator": {"type": "string", "maxLength": 1}, 81 | "payees_name_control": {"type": "string", "maxLength": 4}, 82 | "type_of_tin": {"type": "string", "maxLength": 1}, 83 | "payees_tin": {"$ref": "#/definitions/tin"}, 84 | "payers_account_number_for_payee": {"type": "string", "maxLength": 20}, 85 | "payers_office_code": {"type": "string", "maxLength": 4}, 86 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 87 | "payment_amount_2": {"$ref": "#/definitions/dollar_amount"}, 88 | "payment_amount_3": {"$ref": "#/definitions/dollar_amount"}, 89 | "payment_amount_4": {"$ref": "#/definitions/dollar_amount"}, 90 | "payment_amount_5": {"$ref": "#/definitions/dollar_amount"}, 91 | "payment_amount_6": {"$ref": "#/definitions/dollar_amount"}, 92 | "payment_amount_7": {"$ref": "#/definitions/dollar_amount"}, 93 | "payment_amount_8": {"$ref": "#/definitions/dollar_amount"}, 94 | "payment_amount_9": {"$ref": "#/definitions/dollar_amount"}, 95 | "payment_amount_A": {"$ref": "#/definitions/dollar_amount"}, 96 | "payment_amount_B": {"$ref": "#/definitions/dollar_amount"}, 97 | "payment_amount_C": {"$ref": "#/definitions/dollar_amount"}, 98 | "payment_amount_D": {"$ref": "#/definitions/dollar_amount"}, 99 | "payment_amount_E": {"$ref": "#/definitions/dollar_amount"}, 100 | "payment_amount_F": {"$ref": "#/definitions/dollar_amount"}, 101 | "payment_amount_G": {"$ref": "#/definitions/dollar_amount"}, 102 | "foreign_country_indicator": {"type": "string", "maxLength": 1}, 103 | "first_payee_name_line": {"$ref": "#/definitions/generic_name"}, 104 | "second_payee_name_line": {"$ref": "#/definitions/generic_name"}, 105 | "payee_mailing_address": {"$ref": "#/definitions/address"}, 106 | "payee_city": {"$ref": "#/definitions/city"}, 107 | "payee_state": {"$ref": "#/definitions/state"}, 108 | "payee_zip_code": {"$ref": "#/definitions/zip_code"}, 109 | "record_sequence_number": {"type": "string", "maxLength": 8}, 110 | "second_tin_notice": {"type": "string", "maxLength": 1}, 111 | "direct_sales_indicator": {"type": "string", "maxLength": 1}, 112 | "fatca_filing_requirement_indicator": {"type": "string", "maxLength": 1}, 113 | "special_data_entries": {"type": "string", "maxLength": 60}, 114 | "state_income_tax_withheld": {"type": "string", "maxLength": 12}, 115 | "local_income_tax_withheld": {"type": "string", "maxLength": 12}, 116 | "combined_federal_state_code": {"type": "string", "maxLength": 2} 117 | }, 118 | "required":[ 119 | "first_payee_name_line", "payees_name_control", "payment_year", 120 | "payee_mailing_address", "payee_city", "payee_state", 121 | "payee_zip_code", "payees_tin", "payment_amount_7" 122 | ] 123 | } 124 | }, 125 | "end_of_payer":{ 126 | "type": "object", 127 | "properties":{ 128 | "record_type": {"type": "string", "maxLength": 1}, 129 | "number_of_payees": {"type": "string", "maxLength": 8}, 130 | "payment_amount_1": {"$ref": "#/definitions/dollar_amount"}, 131 | "payment_amount_2": {"$ref": "#/definitions/dollar_amount"}, 132 | "payment_amount_3": {"$ref": "#/definitions/dollar_amount"}, 133 | "payment_amount_4": {"$ref": "#/definitions/dollar_amount"}, 134 | "payment_amount_5": {"$ref": "#/definitions/dollar_amount"}, 135 | "payment_amount_6": {"$ref": "#/definitions/dollar_amount"}, 136 | "payment_amount_7": {"$ref": "#/definitions/dollar_amount"}, 137 | "payment_amount_8": {"$ref": "#/definitions/dollar_amount"}, 138 | "payment_amount_9": {"$ref": "#/definitions/dollar_amount"}, 139 | "payment_amount_A": {"$ref": "#/definitions/dollar_amount"}, 140 | "payment_amount_B": {"$ref": "#/definitions/dollar_amount"}, 141 | "payment_amount_C": {"$ref": "#/definitions/dollar_amount"}, 142 | "payment_amount_D": {"$ref": "#/definitions/dollar_amount"}, 143 | "payment_amount_E": {"$ref": "#/definitions/dollar_amount"}, 144 | "payment_amount_F": {"$ref": "#/definitions/dollar_amount"}, 145 | "payment_amount_G": {"$ref": "#/definitions/dollar_amount"}, 146 | "record_sequence_number": {"type": "string", "maxLength": 8} 147 | } 148 | }, 149 | "end_of_transmission":{ 150 | "type": "object", 151 | "properties":{ 152 | "record_type": {"type": "string", "maxLength": 1}, 153 | "number_of_a_records": {"type": "string", "maxLength": 8}, 154 | "total_number_of_payees": {"type": "string", "maxLength": 8}, 155 | "record_sequence_number": {"type": "string", "maxLength": 8} 156 | } 157 | } 158 | }, 159 | "definitions":{ 160 | "tin":{ 161 | "type":"string", 162 | "pattern":"(^[0-9]{2}[ -]?[0-9]{7}$)|(^[0-9]{3}[ -]?[0-9]{2}[ -]?[0-9]{4}$)" 163 | }, 164 | "state":{ 165 | "type":"string", 166 | "pattern":"^[a-zA-Z]{2}$" 167 | }, 168 | "zip_code":{ 169 | "type:":"string", 170 | "pattern":"^[0-9]{5}(-[0-9]{4})?$" 171 | }, 172 | "email":{ 173 | "type":"string", 174 | "maxLength": 50, 175 | "pattern": "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" 176 | }, 177 | "address":{ 178 | "type": "string", 179 | "maxLength": 40 180 | }, 181 | "city":{ 182 | "type": "string", 183 | "maxLength": 40 184 | }, 185 | "phone":{ 186 | "type": "string", 187 | "pattern": "^\\(?[0-9]{3}\\)?[ .-]?[0-9]{3}[ .-]?[0-9]{4}$" 188 | }, 189 | "year":{ 190 | "type": "string", 191 | "pattern":"^[0-9]{4}", 192 | "maxLength": 4 193 | }, 194 | "generic_name":{ 195 | "type": "string", 196 | "maxLength": 40 197 | }, 198 | "name_control":{ 199 | "type": "string", 200 | "minLength": 4, 201 | "maxLength": 4 202 | }, 203 | "dollar_amount":{ 204 | "type": "string", 205 | "pattern": "^[\\$]?[0-9,]*\\.?[0-9]{2}$" 206 | }, 207 | "foreign_entity": { 208 | "type": "string", 209 | "pattern": "^1?$" 210 | }, 211 | "transmitter_control_code": { 212 | "type": "string", 213 | "pattern": "^[a-zA-Z0-9]{5}$", 214 | "maxLength": 5 215 | } 216 | }, 217 | "additional_properties": false 218 | } 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=pointless-string-statement 58 | 59 | # Enable the message, report, category or checker with the given id(s). You can 60 | # either give multiple identifier separated by comma (,) or put this option 61 | # multiple time (only on the command line, not in the configuration file where 62 | # it should appear only once). See also the "--disable" option for examples. 63 | enable=c-extension-no-member 64 | 65 | 66 | [REPORTS] 67 | 68 | # Python expression which should return a note less than 10 (10 is the highest 69 | # note). You have access to the variables errors warning, statement which 70 | # respectively contain the number of errors / warnings messages and the total 71 | # number of statements analyzed. This is used by the global evaluation report 72 | # (RP0004). 73 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 74 | 75 | # Template used to display messages. This is a python new-style format string 76 | # used to format the message information. See doc for all details 77 | #msg-template= 78 | 79 | # Set the output format. Available formats are text, parseable, colorized, json 80 | # and msvs (visual studio).You can also give a reporter class, eg 81 | # mypackage.mymodule.MyReporterClass. 82 | output-format=text 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=no 86 | 87 | # Activate the evaluation score. 88 | score=yes 89 | 90 | 91 | [REFACTORING] 92 | 93 | # Maximum number of nested blocks for function / method body 94 | max-nested-blocks=5 95 | 96 | 97 | [BASIC] 98 | 99 | # Naming style matching correct argument names 100 | argument-naming-style=snake_case 101 | 102 | # Regular expression matching correct argument names. Overrides argument- 103 | # naming-style 104 | #argument-rgx= 105 | 106 | # Naming style matching correct attribute names 107 | attr-naming-style=snake_case 108 | 109 | # Regular expression matching correct attribute names. Overrides attr-naming- 110 | # style 111 | #attr-rgx= 112 | 113 | # Bad variable names which should always be refused, separated by a comma 114 | bad-names=foo, 115 | bar, 116 | baz, 117 | toto, 118 | tutu, 119 | tata 120 | 121 | # Naming style matching correct class attribute names 122 | class-attribute-naming-style=any 123 | 124 | # Regular expression matching correct class attribute names. Overrides class- 125 | # attribute-naming-style 126 | #class-attribute-rgx= 127 | 128 | # Naming style matching correct class names 129 | class-naming-style=PascalCase 130 | 131 | # Regular expression matching correct class names. Overrides class-naming-style 132 | #class-rgx= 133 | 134 | # Naming style matching correct constant names 135 | const-naming-style=UPPER_CASE 136 | 137 | # Regular expression matching correct constant names. Overrides const-naming- 138 | # style 139 | #const-rgx= 140 | 141 | # Minimum line length for functions/classes that require docstrings, shorter 142 | # ones are exempt. 143 | docstring-min-length=-1 144 | 145 | # Naming style matching correct function names 146 | function-naming-style=snake_case 147 | 148 | # Regular expression matching correct function names. Overrides function- 149 | # naming-style 150 | #function-rgx= 151 | 152 | # Good variable names which should always be accepted, separated by a comma 153 | good-names=i, 154 | j, 155 | k, 156 | ex, 157 | Run, 158 | _ 159 | 160 | # Include a hint for the correct naming format with invalid-name 161 | include-naming-hint=no 162 | 163 | # Naming style matching correct inline iteration names 164 | inlinevar-naming-style=any 165 | 166 | # Regular expression matching correct inline iteration names. Overrides 167 | # inlinevar-naming-style 168 | #inlinevar-rgx= 169 | 170 | # Naming style matching correct method names 171 | method-naming-style=snake_case 172 | 173 | # Regular expression matching correct method names. Overrides method-naming- 174 | # style 175 | #method-rgx= 176 | 177 | # Naming style matching correct module names 178 | module-naming-style=snake_case 179 | 180 | # Regular expression matching correct module names. Overrides module-naming- 181 | # style 182 | #module-rgx= 183 | 184 | # Colon-delimited sets of names that determine each other's naming style when 185 | # the name regexes allow several styles. 186 | name-group= 187 | 188 | # Regular expression which should only match function or class names that do 189 | # not require a docstring. 190 | no-docstring-rgx=^_ 191 | 192 | # List of decorators that produce properties, such as abc.abstractproperty. Add 193 | # to this list to register other decorators that produce valid properties. 194 | property-classes=abc.abstractproperty 195 | 196 | # Naming style matching correct variable names 197 | variable-naming-style=snake_case 198 | 199 | # Regular expression matching correct variable names. Overrides variable- 200 | # naming-style 201 | #variable-rgx= 202 | 203 | 204 | [FORMAT] 205 | 206 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 207 | expected-line-ending-format= 208 | 209 | # Regexp for a line that is allowed to be longer than the limit. 210 | ignore-long-lines=^\s*(# )??$ 211 | 212 | # Number of spaces of indent required inside a hanging or continued line. 213 | indent-after-paren=4 214 | 215 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 216 | # tab). 217 | indent-string=' ' 218 | 219 | # Maximum number of characters on a single line. 220 | max-line-length=100 221 | 222 | # Maximum number of lines in a module 223 | max-module-lines=1000 224 | 225 | # List of optional constructs for which whitespace checking is disabled. `dict- 226 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 227 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 228 | # `empty-line` allows space-only lines. 229 | no-space-check=trailing-comma, 230 | dict-separator 231 | 232 | # Allow the body of a class to be on the same line as the declaration if body 233 | # contains single statement. 234 | single-line-class-stmt=no 235 | 236 | # Allow the body of an if to be on the same line as the test if there is no 237 | # else. 238 | single-line-if-stmt=no 239 | 240 | 241 | [LOGGING] 242 | 243 | # Logging modules to check that the string format arguments are in logging 244 | # function parameter format 245 | logging-modules=logging 246 | 247 | 248 | [MISCELLANEOUS] 249 | 250 | # List of note tags to take in consideration, separated by a comma. 251 | notes=FIXME, 252 | XXX, 253 | TODO 254 | 255 | 256 | [SIMILARITIES] 257 | 258 | # Ignore comments when computing similarities. 259 | ignore-comments=yes 260 | 261 | # Ignore docstrings when computing similarities. 262 | ignore-docstrings=yes 263 | 264 | # Ignore imports when computing similarities. 265 | ignore-imports=yes 266 | 267 | # Minimum lines number of a similarity. 268 | min-similarity-lines=4 269 | 270 | 271 | [SPELLING] 272 | 273 | # Limits count of emitted suggestions for spelling mistakes 274 | max-spelling-suggestions=4 275 | 276 | # Spelling dictionary name. Available dictionaries: none. To make it working 277 | # install python-enchant package. 278 | spelling-dict= 279 | 280 | # List of comma separated words that should not be checked. 281 | spelling-ignore-words= 282 | 283 | # A path to a file that contains private dictionary; one word per line. 284 | spelling-private-dict-file= 285 | 286 | # Tells whether to store unknown words to indicated private dictionary in 287 | # --spelling-private-dict-file option instead of raising a message. 288 | spelling-store-unknown-words=no 289 | 290 | 291 | [TYPECHECK] 292 | 293 | # List of decorators that produce context managers, such as 294 | # contextlib.contextmanager. Add to this list to register other decorators that 295 | # produce valid context managers. 296 | contextmanager-decorators=contextlib.contextmanager 297 | 298 | # List of members which are set dynamically and missed by pylint inference 299 | # system, and so shouldn't trigger E1101 when accessed. Python regular 300 | # expressions are accepted. 301 | generated-members= 302 | 303 | # Tells whether missing members accessed in mixin class should be ignored. A 304 | # mixin class is detected if its name ends with "mixin" (case insensitive). 305 | ignore-mixin-members=yes 306 | 307 | # This flag controls whether pylint should warn about no-member and similar 308 | # checks whenever an opaque object is returned when inferring. The inference 309 | # can return multiple potential results while evaluating a Python object, but 310 | # some branches might not be evaluated, which results in partial inference. In 311 | # that case, it might be useful to still emit no-member and other checks for 312 | # the rest of the inferred objects. 313 | ignore-on-opaque-inference=yes 314 | 315 | # List of class names for which member attributes should not be checked (useful 316 | # for classes with dynamically set attributes). This supports the use of 317 | # qualified names. 318 | ignored-classes=optparse.Values,thread._local,_thread._local 319 | 320 | # List of module names for which member attributes should not be checked 321 | # (useful for modules/projects where namespaces are manipulated during runtime 322 | # and thus existing member attributes cannot be deduced by static analysis. It 323 | # supports qualified module names, as well as Unix pattern matching. 324 | ignored-modules= 325 | 326 | # Show a hint with possible names when a member name was not found. The aspect 327 | # of finding the hint is based on edit distance. 328 | missing-member-hint=yes 329 | 330 | # The minimum edit distance a name should have in order to be considered a 331 | # similar match for a missing member name. 332 | missing-member-hint-distance=1 333 | 334 | # The total number of similar names that should be taken in consideration when 335 | # showing a hint for a missing member. 336 | missing-member-max-choices=1 337 | 338 | 339 | [VARIABLES] 340 | 341 | # List of additional names supposed to be defined in builtins. Remember that 342 | # you should avoid to define new builtins when possible. 343 | additional-builtins= 344 | 345 | # Tells whether unused global variables should be treated as a violation. 346 | allow-global-unused-variables=yes 347 | 348 | # List of strings which can identify a callback function by name. A callback 349 | # name must start or end with one of those strings. 350 | callbacks=cb_, 351 | _cb 352 | 353 | # A regular expression matching the name of dummy variables (i.e. expectedly 354 | # not used). 355 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 356 | 357 | # Argument names that match this expression will be ignored. Default to name 358 | # with leading underscore 359 | ignored-argument-names=_.*|^ignored_|^unused_ 360 | 361 | # Tells whether we should check for unused import in __init__ files. 362 | init-import=no 363 | 364 | # List of qualified module names which can have objects that can redefine 365 | # builtins. 366 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 367 | 368 | 369 | [CLASSES] 370 | 371 | # List of method names used to declare (i.e. assign) instance attributes. 372 | defining-attr-methods=__init__, 373 | __new__, 374 | setUp 375 | 376 | # List of member names, which should be excluded from the protected access 377 | # warning. 378 | exclude-protected=_asdict, 379 | _fields, 380 | _replace, 381 | _source, 382 | _make 383 | 384 | # List of valid names for the first argument in a class method. 385 | valid-classmethod-first-arg=cls 386 | 387 | # List of valid names for the first argument in a metaclass class method. 388 | valid-metaclass-classmethod-first-arg=mcs 389 | 390 | 391 | [DESIGN] 392 | 393 | # Maximum number of arguments for function / method 394 | max-args=5 395 | 396 | # Maximum number of attributes for a class (see R0902). 397 | max-attributes=7 398 | 399 | # Maximum number of boolean expressions in a if statement 400 | max-bool-expr=5 401 | 402 | # Maximum number of branch for function / method body 403 | max-branches=12 404 | 405 | # Maximum number of locals for function / method body 406 | max-locals=15 407 | 408 | # Maximum number of parents for a class (see R0901). 409 | max-parents=7 410 | 411 | # Maximum number of public methods for a class (see R0904). 412 | max-public-methods=20 413 | 414 | # Maximum number of return / yield for function / method body 415 | max-returns=6 416 | 417 | # Maximum number of statements in function / method body 418 | max-statements=50 419 | 420 | # Minimum number of public methods for a class (see R0903). 421 | min-public-methods=2 422 | 423 | 424 | [IMPORTS] 425 | 426 | # Allow wildcard imports from modules that define __all__. 427 | allow-wildcard-with-all=no 428 | 429 | # Analyse import fallback blocks. This can be used to support both Python 2 and 430 | # 3 compatible code, which means that the block might have code that exists 431 | # only in one or another interpreter, leading to false positives when analysed. 432 | analyse-fallback-blocks=no 433 | 434 | # Deprecated modules which should not be used, separated by a comma 435 | deprecated-modules=optparse,tkinter.tix 436 | 437 | # Create a graph of external dependencies in the given file (report RP0402 must 438 | # not be disabled) 439 | ext-import-graph= 440 | 441 | # Create a graph of every (i.e. internal and external) dependencies in the 442 | # given file (report RP0402 must not be disabled) 443 | import-graph= 444 | 445 | # Create a graph of internal dependencies in the given file (report RP0402 must 446 | # not be disabled) 447 | int-import-graph= 448 | 449 | # Force import order to recognize a module as part of the standard 450 | # compatibility libraries. 451 | known-standard-library= 452 | 453 | # Force import order to recognize a module as part of a third party library. 454 | known-third-party=enchant 455 | 456 | 457 | [EXCEPTIONS] 458 | 459 | # Exceptions that will emit a warning when being caught. Defaults to 460 | # "Exception" 461 | overgeneral-exceptions=Exception 462 | --------------------------------------------------------------------------------