diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..89c2554
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+.venv
+.pytest_cache
diff --git a/.envExemple b/.envExemple
new file mode 100644
index 0000000..899c9c9
--- /dev/null
+++ b/.envExemple
@@ -0,0 +1,9 @@
+#database envs
+DATABASE_URL=""
+DATABASE_URL_DOCKER=""
+
+#app
+APP_NAME=""
+DEV=""
+APP_DOCKER_RUN=""
+APP_TEST=""
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..30a2b92
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,162 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+requisitos_do_sistema
+
+documento de requisitos
+
+/data
+
+.vscode
+
+/venhapararecomb/static/xmlDocs/*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..580ff71
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,21 @@
+FROM python:3.10 as requirements-stage
+
+WORKDIR /tmp
+
+RUN pip install poetry
+
+COPY ./pyproject.toml ./poetry.lock* /tmp/
+
+RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
+
+FROM python:3.10
+
+WORKDIR /venhapararecomb
+
+COPY --from=requirements-stage /tmp/requirements.txt /venhapararecomb/requirements.txt
+
+RUN pip install --no-cache-dir --upgrade -r /venhapararecomb/requirements.txt
+
+COPY . /venhapararecomb
+
+CMD ["uvicorn", "app.main:app", "--host=0.0.0.0", "--port=8000", "--reload"]
\ No newline at end of file
diff --git a/NFe-002-3103.xml b/NFe-002-3103.xml
deleted file mode 100644
index 831d193..0000000
--- a/NFe-002-3103.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-3100464032Vendas a prazo55231032019-04-10T17:24:03-02:002019-04-11T17:17:30-02:001131702061171101000106273476000182MECA Office Mobil. Eireli-MEMECA Office Mobil. Eireli-MEAV. MARCOS DE FREITAS COSTA1055DANIEL FOSECA3170206UberlandiaMG384003281058BRASIL34323855857022916720058125587387000155HLTS ENGENHARIA E CONSTRUCOES LTDARUA MACHADO DE ASSIS1324LIDICE3170206UberlandiaMG384000811058BRASIL34322359661702177134005400331SEM GTINCADEIRA GIRATORIA S/ BRACO ALMOFADADA PRETO940190905102UN2165.00000330.00SEM GTINUN2165.00000177.6801020808IMOBILIZADO IMOB - 2317 CADEIRA GIRATORIA S/ BRACO ALMOFADADA PRETO - IMOBILIZADO EQUIP. MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO.01228SEM GTINCADEIRA GIRATORIA C/ BRACO ALMOFADADA PRETO940190905102UN1215.00000215.00SEM GTINUN1215.00000150.6101020808IMOBILIZADO EQUIP/MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO01324SEM GTINBANQUETA ALTA 70CM PARA BALCAO ASSENTO 30 OU940190905102PC190.0000090.00SEM GTINPC190.00000121.190102080840CM NA COR PRETA - IMOBILIZADO EQUIP./MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO01644SEM GTINESTANTE ACO C/ 06 PRATELEIRAS 0,93X040X1,98M940690205102PC2234.00000468.00SEM GTINPC2234.00000145.8701020808(REFORCADA) AMAPA - IMOBILIZADO.0.000.000.000.000.000.000.000.001103.000.000.000.000.000.000.000.000.000.001103.00195.3506VOLUMEVARIAS0.0000.0000000031031103.000.001103.000012019-05-111103.00141103.000.00ORDEM DE FORNECIMENTO 36994 - 28DD - INFORMACOES COMPLEMENTARES a seguinte informacao. EMPRESA ENQUADRADA NO SIMPLES NACIONAL. NAO GERA CREDITO DE IPI/ISS. GERA CREDITO DE ICMS. Trib aprox R$: 54,84 Federal 140,51 Estadual Fonte: IBPT empresometro.com.br S3A6R4ngqVwH6QNCAHyRuI529RIAr7Nyk=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
- 1
- 14.2.26
- 31190406273476000182550020000031031004640327
- 2019-04-10T17:23:27-03:00
- 131193257591884
- ngqVwH6QNCAHyRuI529RIAr7Nyk=
- 100
- Autorizado o uso da NF-e
-
-
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..6a00d33
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-8000}
\ No newline at end of file
diff --git a/README.md b/README.md
index 817b8c7..91a3449 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,65 @@
+
+
Projeto: Venha para Recomb
+
+## Preview
+
+
+## Badges
+
+
+
+
+
+
+
+
+
+
+## Índice
+
+* [Preview](#preview)
+* [Badges](#badges)
+* [Índice](#índice)
+* [Desafio](#desafio)
+* [Configuração](#configuração)
+* [Status do Projeto](#status-do-Projeto)
+* [Tecnologias Ultilizadas](#tecnologias-ultilizadas)
+* [Pessoas Desenvolvedoras do Projeto](#pessoas-desenvolvedoras)
+
+
+## Status do Projeto
+
+No momento o projeto se encontra finalizado, podendo no futuro ter alguma adição de funcionalidades.
+
+
+
+## ⚙ Configuração
+
+1- Para baixar o projeto:
+> git clone https://github.com/alexandrejastrow/venhapararecomb.git
+
+2- Variáveis de ambiente:
+> Crie um arquivo .env e use o .envExemple como base
+
+3- Para iniciar a aplicação:
+> docker-compose up
+
+## ✔️ Tecnologias Ultilizadas
+
+- ``Python 3.10``
+- ``FastAPI``
+- ``Postgres``
+- ``SQLAlchemy``
+- ``Alembic``
+- ``Jinja2``
+- ``Docker``
+- ``docker-compose``
+- ``Bootstrap``
+- ``HTML5``
+- ``CSS3``
+#
+
+## Desafio
# Venha para Recomb
O desafio é desenvolver um programa que permita realizar as seguintes buscas:
@@ -60,3 +122,6 @@ O candidato será desclassifiado nas seguintes situações:
+## Pessoas Desenvolvedoras do Projeto
+
+[ Alexandre Jastrow da Cruz](https://github.com/alexandrejastrow)
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..3018330
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,101 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = app/alembic
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to app/alembic/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:app/alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+sqlalchemy.url = postgresql://postgres:123456@localhost:5432/postgres
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..b794fd4
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1 @@
+__version__ = '0.1.0'
diff --git a/app/alembic/README b/app/alembic/README
new file mode 100644
index 0000000..98e4f9c
--- /dev/null
+++ b/app/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/app/alembic/env.py b/app/alembic/env.py
new file mode 100644
index 0000000..55b2529
--- /dev/null
+++ b/app/alembic/env.py
@@ -0,0 +1,80 @@
+from app.infra.sqlalchemy.config.database import Base
+from app.infra.sqlalchemy.models.models import *
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/app/alembic/script.py.mako b/app/alembic/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/app/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/app/alembic/versions/032b1b2ccb94_.py b/app/alembic/versions/032b1b2ccb94_.py
new file mode 100644
index 0000000..2f8d520
--- /dev/null
+++ b/app/alembic/versions/032b1b2ccb94_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 032b1b2ccb94
+Revises: 38d82987e1be
+Create Date: 2022-03-25 20:00:57.788644
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '032b1b2ccb94'
+down_revision = '38d82987e1be'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/app/alembic/versions/38d82987e1be_db_tables.py b/app/alembic/versions/38d82987e1be_db_tables.py
new file mode 100644
index 0000000..44bc27b
--- /dev/null
+++ b/app/alembic/versions/38d82987e1be_db_tables.py
@@ -0,0 +1,75 @@
+"""db tables
+
+Revision ID: 38d82987e1be
+Revises:
+Create Date: 2022-03-24 17:19:53.880206
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '38d82987e1be'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('person',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(), nullable=False),
+ sa.Column('cpf', sa.String(), nullable=True),
+ sa.Column('cnpj', sa.String(), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('cnpj'),
+ sa.UniqueConstraint('cpf')
+ )
+ op.create_index(op.f('ix_person_id'), 'person', ['id'], unique=False)
+ op.create_table('address',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('logradouro', sa.String(), nullable=True),
+ sa.Column('numero', sa.Integer(), nullable=True),
+ sa.Column('bairro', sa.String(), nullable=True),
+ sa.Column('municipio', sa.String(), nullable=True),
+ sa.Column('uf', sa.String(), nullable=True),
+ sa.Column('cep', sa.String(), nullable=True),
+ sa.Column('pais', sa.String(), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.Column('fk_person_address', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['fk_person_address'], ['person.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_address_id'), 'address', ['id'], unique=False)
+ op.create_table('nfe',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('nfe_id', sa.String(), nullable=False),
+ sa.Column('date_venc', sa.DateTime(), nullable=False),
+ sa.Column('total', sa.Float(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
+ sa.Column('fk_person_provider_nfe', sa.Integer(), nullable=True),
+ sa.Column('fk_person_client_nfe', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['fk_person_client_nfe'], ['person.id'], ),
+ sa.ForeignKeyConstraint(['fk_person_provider_nfe'], ['person.id'], ),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('nfe_id')
+ )
+ op.create_index(op.f('ix_nfe_id'), 'nfe', ['id'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_nfe_id'), table_name='nfe')
+ op.drop_table('nfe')
+ op.drop_index(op.f('ix_address_id'), table_name='address')
+ op.drop_table('address')
+ op.drop_index(op.f('ix_person_id'), table_name='person')
+ op.drop_table('person')
+ # ### end Alembic commands ###
diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/get_nfe_controller.py b/app/controllers/get_nfe_controller.py
new file mode 100644
index 0000000..eaed2ba
--- /dev/null
+++ b/app/controllers/get_nfe_controller.py
@@ -0,0 +1,93 @@
+from fastapi import HTTPException, status
+from sqlalchemy.orm import Session
+from typing import List
+from app.schemas import schemas
+from app.infra.sqlalchemy.reposipories import nfe_repository
+from app.infra.sqlalchemy.reposipories import person_repository
+from app.infra.sqlalchemy.reposipories import address_repository
+
+
+def get_nfe(db: Session, nfe_id: str) -> dict:
+ """Function responsible for obtaining information regarding the NFe and returning it formatted.
+ Args:
+ db (Session): database session
+
+ Returns:
+ dict: Returns all information regarding an NFe.
+ """
+ nfe = nfe_repository.get_nfe_by_nfe_id(db, nfe_id)
+
+ if(not nfe):
+ return HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail={"message": "this document does not exist."}
+ )
+
+ provider = person_repository.get_person(db, nfe.provider_id)
+ client = person_repository.get_person(db, nfe.client_id)
+ address_pro = address_repository.get_address_by_person_id(
+ db, nfe.provider_id)
+ address_cli = address_repository.get_address_by_person_id(
+ db, nfe.client_id)
+
+ add_prov = schemas.AddressView(
+ id=address_pro.id,
+ logradouro=address_pro.logradouro,
+ numero=address_pro.numero,
+ bairro=address_pro.bairro,
+ municipio=address_pro.municipio,
+ uf=address_pro.uf,
+ cep=address_pro.cep,
+ pais=address_pro.pais
+ )
+ add_cli = schemas.AddressView(
+ id=address_cli.id,
+ logradouro=address_cli.logradouro,
+ numero=address_cli.numero,
+ bairro=address_cli.bairro,
+ municipio=address_cli.municipio,
+ uf=address_cli.uf,
+ cep=address_cli.cep,
+ pais=address_cli.pais
+ )
+ per_cli = schemas.PersonView(
+ id=client.id,
+ name=client.name,
+ cpf=client.cpf,
+ cnpj=client.cnpj,
+ address=add_cli,
+ )
+ per_prov = schemas.PersonView(
+ id=provider.id,
+ name=provider.name,
+ cpf=provider.cpf,
+ cnpj=provider.cnpj,
+ address=add_prov,
+ )
+ data = schemas.NFeView(
+ id=nfe.id,
+ nfe_id=nfe.nfe_id,
+ date_venc=nfe.date_venc,
+ total=nfe.total,
+ provider=per_prov,
+ client=per_cli,
+ )
+
+ return data
+
+
+def get_all_nfe(db: Session) -> List[dict]:
+ """Function responsible for getting the information already formatted regarding the NFe and returning them.
+
+ Args:
+ db (Session): database session
+
+ Returns:
+ List[dict]: returns a list with all information for all NFe.
+ """
+ nfes = nfe_repository.get_all_nfe(db)
+ lst = []
+ for nfe in nfes:
+ lst.append(get_nfe(db, nfe.nfe_id))
+ data = reversed(lst)
+ return data
diff --git a/app/infra/__init__.py b/app/infra/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/infra/settings.py b/app/infra/settings.py
new file mode 100644
index 0000000..4258ede
--- /dev/null
+++ b/app/infra/settings.py
@@ -0,0 +1,29 @@
+from functools import lru_cache
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+ # app envs
+ app_name: str = "FastApi"
+ app_docker_run: str | None = None
+ app_test: str | None = None
+ dev: str
+ # database envs
+ database_url: str
+ database_url_docker: str | None = None
+
+ class Config:
+ env_file = ".env"
+
+
+@lru_cache()
+def get_settings() -> Settings:
+ """Capture environment variables
+
+ Returns:
+ Returns environment variables
+ """
+ return Settings()
+
+
+settings = get_settings()
diff --git a/app/infra/sqlalchemy/config/__init__.py b/app/infra/sqlalchemy/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/infra/sqlalchemy/config/database.py b/app/infra/sqlalchemy/config/database.py
new file mode 100644
index 0000000..2cb31b8
--- /dev/null
+++ b/app/infra/sqlalchemy/config/database.py
@@ -0,0 +1,26 @@
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from app.infra.settings import settings
+
+
+if settings.dev == "true":
+ if settings.app_docker_run == "true":
+ SQLALCHEMY_DATABASE_URL = settings.database_url_docker
+ else:
+ SQLALCHEMY_DATABASE_URL = settings.database_url
+
+else:
+ url = settings.database_url
+ if url and url.startswith("postgres://"):
+ SQLALCHEMY_DATABASE_URL = url.replace(
+ "postgres://", "postgresql://", 1)
+
+engine = create_engine(
+ SQLALCHEMY_DATABASE_URL
+)
+
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+
+Base = declarative_base()
diff --git a/app/infra/sqlalchemy/models/__init__.py b/app/infra/sqlalchemy/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/infra/sqlalchemy/models/models.py b/app/infra/sqlalchemy/models/models.py
new file mode 100644
index 0000000..c5f79ff
--- /dev/null
+++ b/app/infra/sqlalchemy/models/models.py
@@ -0,0 +1,56 @@
+
+from sqlalchemy import Column, Integer, String, DateTime, Float, ForeignKey
+from sqlalchemy.sql import func
+from sqlalchemy.orm import relationship
+from ..config.database import Base
+
+
+class Person(Base):
+ __tablename__ = "person"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String, nullable=False)
+ cpf = Column(String, nullable=True, unique=True)
+ cnpj = Column(String, nullable=True, unique=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True),
+ server_default=func.now(), onupdate=func.now())
+
+ address = relationship("Address", back_populates='person')
+
+
+class Address(Base):
+ __tablename__ = "address"
+
+ id = Column(Integer, primary_key=True, index=True)
+ logradouro = Column(String, nullable=True)
+ numero = Column(Integer, nullable=True)
+ bairro = Column(String, nullable=True)
+ municipio = Column(String, nullable=True)
+ uf = Column(String, nullable=True)
+ cep = Column(String, nullable=True)
+ pais = Column(String, nullable=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True),
+ server_default=func.now(), onupdate=func.now())
+ person_id = Column(Integer, ForeignKey(
+ 'person.id'), name='fk_person_address')
+
+ person = relationship("Person", back_populates='address')
+
+
+class NFe(Base):
+ __tablename__ = "nfe"
+
+ id = Column(Integer, primary_key=True, index=True)
+ nfe_id = Column(String, nullable=False, unique=True)
+ date_venc = Column(DateTime, nullable=False)
+ total = Column(Float, nullable=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True),
+ server_default=func.now(), onupdate=func.now())
+
+ provider_id = Column(Integer, ForeignKey('person.id'),
+ name='fk_person_provider_nfe')
+ client_id = Column(Integer, ForeignKey('person.id'),
+ name='fk_person_client_nfe')
diff --git a/app/infra/sqlalchemy/reposipories/__init__.py b/app/infra/sqlalchemy/reposipories/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/infra/sqlalchemy/reposipories/address_repository.py b/app/infra/sqlalchemy/reposipories/address_repository.py
new file mode 100644
index 0000000..4747033
--- /dev/null
+++ b/app/infra/sqlalchemy/reposipories/address_repository.py
@@ -0,0 +1,18 @@
+from sqlalchemy.orm import Session
+
+from app.infra.sqlalchemy.models import models
+from app.schemas import schemas
+
+
+def create_address(db: Session, address: schemas.AddressCreate, id_person):
+
+ db_address = models.Address(**address)
+ db_address.person_id = id_person
+ db.add(db_address)
+ db.commit()
+ db.refresh(db_address)
+ return db_address
+
+
+def get_address_by_person_id(db: Session, id_person):
+ return db.query(models.Address).filter(models.Address.person_id == id_person).first()
diff --git a/app/infra/sqlalchemy/reposipories/nfe_repository.py b/app/infra/sqlalchemy/reposipories/nfe_repository.py
new file mode 100644
index 0000000..5d09406
--- /dev/null
+++ b/app/infra/sqlalchemy/reposipories/nfe_repository.py
@@ -0,0 +1,31 @@
+from sqlalchemy.orm import Session
+
+from app.infra.sqlalchemy.models import models
+from app.schemas import schemas
+
+
+def create_nfe(db: Session, nfe: schemas.NfeCreate, provider_id: int, client_id: int):
+
+ db_nfe = models.NFe(**nfe)
+ db_nfe.client_id = client_id
+ db_nfe.provider_id = provider_id
+ db.add(db_nfe)
+ db.commit()
+ db.refresh(db_nfe)
+ return db_nfe
+
+
+def get_nfe_by_provider_id(db: Session, provider_id: int):
+ return db.query(models.NFe).filter(models.NFe.provider_id == provider_id).first()
+
+
+def get_nfe_by_client_id(db: Session, client_id: int):
+ return db.query(models.NFe).filter(models.NFe.client_id == client_id).first()
+
+
+def get_nfe_by_nfe_id(db: Session, nfe_id: str):
+ return db.query(models.NFe).filter(models.NFe.nfe_id == nfe_id).first()
+
+
+def get_all_nfe(db: Session):
+ return db.query(models.NFe).all()
diff --git a/app/infra/sqlalchemy/reposipories/person_repository.py b/app/infra/sqlalchemy/reposipories/person_repository.py
new file mode 100644
index 0000000..839bfa8
--- /dev/null
+++ b/app/infra/sqlalchemy/reposipories/person_repository.py
@@ -0,0 +1,27 @@
+from sqlalchemy.orm import Session
+
+from app.infra.sqlalchemy.models import models
+from app.schemas import schemas
+
+
+def get_person(db: Session, person_id: int):
+ return db.query(models.Person).filter(models.Person.id == person_id).first()
+
+
+def get_person_by_document(db: Session, cnpj: str, cpf: str):
+ if(cnpj):
+ return db.query(models.Person).filter(models.Person.cnpj == cnpj).first()
+ return db.query(models.Person).filter(models.Person.cpf == cpf).first()
+
+
+def get_persons(db: Session, skip: int = 0, limit: int = 100):
+ return db.query(models.Person).offset(skip).limit(limit).all()
+
+
+def create_person(db: Session, person: schemas.PersonCreate):
+
+ db_person = models.Person(**person)
+ db.add(db_person)
+ db.commit()
+ db.refresh(db_person)
+ return db_person
diff --git a/app/main.py b/app/main.py
new file mode 100644
index 0000000..a70c624
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,14 @@
+from fastapi import FastAPI
+from app.infra.settings import settings
+from app.router import router
+from app import __version__
+
+
+app = FastAPI(
+ title=settings.app_name,
+ version=__version__,
+ redoc_url=None,
+)
+
+# records existing routes in the routes file
+app.include_router(router)
diff --git a/app/provider/__init__.py b/app/provider/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/provider/save_xml.py b/app/provider/save_xml.py
new file mode 100644
index 0000000..87a7744
--- /dev/null
+++ b/app/provider/save_xml.py
@@ -0,0 +1,166 @@
+from sqlalchemy.orm import Session
+import xml.etree.ElementTree as Et
+from xml.dom import minidom
+from typing import List
+from app.infra.sqlalchemy.reposipories import person_repository
+from app.infra.sqlalchemy.reposipories import address_repository
+from app.infra.sqlalchemy.reposipories import nfe_repository
+
+
+def get_address(xml, pos) -> dict:
+ """Extract information about a person's address (supplier, customer).
+
+ Args:
+ xml (str): Is a string from an xml file
+ pos (int): This variable indicates which address will be returned.
+ Returns:
+ dict: Returns a dictionary with information about a person's address.
+ """
+ logradouro = xml.getElementsByTagName("xLgr")
+ numero = xml.getElementsByTagName("nro")
+ bairro = xml.getElementsByTagName("xBairro")
+ municipio = xml.getElementsByTagName("xMun")
+ uf = xml.getElementsByTagName("UF")
+ cep = xml.getElementsByTagName("CEP")
+ pais = xml.getElementsByTagName("xPais")
+ address = {
+ "logradouro": logradouro[pos].firstChild.data,
+ "numero": numero[pos].firstChild.data,
+ "bairro": bairro[pos].firstChild.data,
+ "municipio": municipio[pos].firstChild.data,
+ "uf": uf[pos].firstChild.data,
+ "cep": cep[pos].firstChild.data,
+ "pais": pais[pos].firstChild.data,
+ }
+ return address
+
+
+def get_people(xml, pos) -> dict:
+ """Extract information about a person (supplier, customer).
+
+ Args:
+ xml (str): Is a string from an xml file
+ pos (int): this variable indicates which person of the found will be returned.
+ Returns:
+ dict: Returns a dictionary with information about a person.
+ """
+ name = xml.getElementsByTagName("xNome")
+ cpf = xml.getElementsByTagName("CPF")
+ cnpj = xml.getElementsByTagName("CNPJ")
+ person = {
+ "name": name[pos].firstChild.data
+ }
+ if(cpf == []):
+ person['cpf'] = None
+ else:
+ person['cpf'] = cpf[pos].firstChild.data
+
+ if(cnpj == []):
+ person['cnpj'] = None
+ else:
+ person['cnpj'] = cnpj[pos].firstChild.data
+
+ return person
+
+
+def get_NFe_info(xml) -> dict:
+ """Extract the information regarding the NFe
+
+ Args:
+ xml (str): Is a string from an xml file
+
+ Returns:
+ dict: Returns a dictionary with information regarding Nfe
+ """
+
+ date_venc = xml.getElementsByTagName("dVenc")
+ total = xml.getElementsByTagName("vLiq")
+
+ nfe = {
+ "date_venc": date_venc[0].firstChild.data,
+ "total": total[0].firstChild.data,
+ }
+
+ return nfe
+
+
+def dismember_xml(file: bytes, db: Session) -> None:
+ """_summary_
+
+ Args:
+ file (bytes): file in binary format
+ db (Session): database session
+ Returns:
+ None
+ """
+ # convert a binary to string
+ xml = Et.fromstring(file)
+
+ # capture the id of the NFe
+ for x in xml[0]:
+ if x.get('Id'):
+ nfe_id = x.get('Id')
+
+ # if the NFe has already been created returns
+ if nfe_repository.get_nfe_by_nfe_id(db, nfe_id):
+ return
+
+ xml = minidom.parseString(file)
+
+ # capture the information in the NFe by the tags
+ enderEmit = get_address(xml, 0)
+ enderDest = get_address(xml, 1)
+
+ provider = get_people(xml, 0)
+ client = get_people(xml, 1)
+
+ nfe = get_NFe_info(xml)
+ nfe['nfe_id'] = nfe_id
+
+ # search for a person by CNPJ and CPF
+ db_provider = person_repository.get_person_by_document(
+ db, cnpj=provider.get('cnpj'), cpf=provider.get('cpf'))
+ db_client = person_repository.get_person_by_document(
+ db, cnpj=client.get('cnpj'), cpf=client.get('cpf'))
+
+ # if it doesn't exist create a new person
+ if not db_client:
+ db_client = person_repository.create_person(db, client)
+ # if it doesn't exist create a new person
+ if not db_provider:
+ db_provider = person_repository.create_person(db, provider)
+
+ if not address_repository.get_address_by_person_id(db, db_provider.id):
+ # save the person's address
+ address_repository.create_address(
+ db, enderEmit, db_provider.id)
+
+ if not address_repository.get_address_by_person_id(db, db_client.id):
+ # save the person's address
+ address_repository.create_address(
+ db, enderDest, db_client.id)
+
+ # create an Nfe
+
+ nfe_repository.create_nfe(db, nfe, db_provider.id, db_client.id)
+
+ return None
+
+
+async def save_xml(files: List[bytes], db: Session) -> None:
+ """_summary_
+
+ Args:
+ files (List[bytes]): A list of files in binary format
+ db (Session): database session
+
+ Returns:
+ None
+ """
+
+ for file in files:
+ try:
+ if(file.decode("utf-8")[:5] == " Session:
+ """Function responsible for capturing the environment variables
+
+ Parameters:
+ None
+
+ Returns:
+ Session: Returns a database session
+
+ """
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+
+router = APIRouter()
+
+
+templates = Jinja2Templates(directory="app/templates")
+
+
+@router.get("/", response_class=HTMLResponse)
+async def home(request: Request, db: Session = Depends(get_db)) -> _TemplateResponse:
+ """Function responsible for receiving a request in the "/" and returns an html template.
+
+ Parameters:
+ request (Request): Request class
+ db (Session): database session
+
+ Returns:
+ _TemplateResponse: Returns an HTML template
+
+ """
+
+ # gets all related data from all NFe.
+ data = get_nfe_controller.get_all_nfe(db)
+ return templates.TemplateResponse("home.html", {"request": request, "id": "id", "data": data})
+
+
+@router.get("/api/xml/{nfe_id}")
+async def list_xml(nfe_id: str, db: Session = Depends(get_db)) -> dict:
+ """Function responsible for receiving a request in the "/api/xml/{nfe_id}" and returns an html template.
+
+ Parameters:
+ nfe_id (str): id of one Nfe
+ db (Session): database session
+
+ Returns:
+ dict: Returns a dictionary that will be parsed in json
+
+ """
+
+ # get all data related to an NFe
+ result = get_nfe_controller.get_nfe(db, nfe_id)
+ return result
+
+
+@router.get("/files/", response_class=FileResponse)
+async def file_example() -> str:
+ """route that returns a valid XML example.
+
+ Returns:
+ returns a string that is parsed to a FIleResponse.
+ """
+ if randint(0, 1):
+ return "test_1.xml"
+ return "test_2.xml"
+
+
+@router.post("/files/")
+async def create_upload_files(background_tasks: BackgroundTasks, files: list[bytes] = File(...), db: Session = Depends(get_db)
+ ) -> RedirectResponse:
+ """Function responsible for receiving a request in "/files/" and redirecting it to "/" after forwarding the data for processing.
+
+ Parameters:
+ background_tasks (BackgroundTasks): BackgroundTasks
+ files (list[bytes]): list[bytes]
+ db (Session): database session
+
+ Returns:
+ RedirectResponse: Redirect to root
+
+ """
+ # start a background task for data processing
+ background_tasks.add_task(save_xml, files, db)
+ return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER)
diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py
new file mode 100644
index 0000000..93fe0bd
--- /dev/null
+++ b/app/schemas/schemas.py
@@ -0,0 +1,79 @@
+from datetime import datetime
+from pydantic import BaseModel
+
+
+class PersonCreate(BaseModel):
+ """
+ schema of creating a person in the database
+ """
+ name: str
+ cpf: str | None = None
+ cnpj: str | None = None
+
+ class Config:
+ orm_mode = True
+
+
+class AddressCreate(BaseModel):
+ """
+ schema of creating a person in the database
+ """
+ logradouro: str | None = None
+ numero: int | None = None
+ bairro: str | None = None
+ municipio: str | None = None
+ uf: str | None = None
+ cep: str | None = None
+ pais: str | None = None
+
+ class Config:
+ orm_mode = True
+
+
+class AddressView(BaseModel):
+ """route response schema
+ """
+ id: int
+ logradouro: str | None = None
+ numero: int | None = None
+ bairro: str | None = None
+ municipio: str | None = None
+ uf: str | None = None
+ cep: str | None = None
+ pais: str | None = None
+
+
+class PersonView(BaseModel):
+ """route response schema
+ """
+ id: int
+ name: str
+ cpf: str | None = None
+ cnpj: str | None = None
+ address: AddressView
+
+
+class NFeView(BaseModel):
+ """route response schema
+ """
+
+ id: int
+ nfe_id: str
+ date_venc: datetime
+ total: float
+ provider: PersonView
+ client: PersonView
+
+
+class NfeCreate(BaseModel):
+ """
+ schema of creating a person in the database
+ """
+ nfe_id: str
+ date_venc: datetime
+ total: float
+ provider_id: int
+ client_id: int
+
+ class Config:
+ orm_mode = True
diff --git a/app/templates/base.html b/app/templates/base.html
new file mode 100644
index 0000000..5643a76
--- /dev/null
+++ b/app/templates/base.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Venha para Recomb
+
+
+
+
+ {%block content%}
+
+ {%endblock%}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/home.html b/app/templates/home.html
new file mode 100644
index 0000000..8b7e930
--- /dev/null
+++ b/app/templates/home.html
@@ -0,0 +1,81 @@
+{% extends 'base.html' %}
+{% block content %}
+
+
+