├── .gitignore ├── LICENSE ├── README.md ├── backend ├── docker-compose.yaml ├── gateway │ ├── .dockerignore │ ├── .env │ ├── Dockerfile │ ├── requirements.txt │ └── src │ │ ├── __init__.py │ │ ├── application │ │ ├── __init__.py │ │ ├── app.py │ │ ├── dependencies.py │ │ ├── exceptions.py │ │ ├── routes │ │ │ ├── __init__.py │ │ │ ├── account │ │ │ │ ├── __init__.py │ │ │ │ └── profile.py │ │ │ ├── auth │ │ │ │ ├── __init__.py │ │ │ │ └── registration.py │ │ │ ├── customer │ │ │ │ ├── __init__.py │ │ │ │ └── address.py │ │ │ ├── driver │ │ │ │ ├── __init__.py │ │ │ │ ├── location.py │ │ │ │ ├── online_status.py │ │ │ │ └── vehicle.py │ │ │ ├── order │ │ │ │ ├── __init__.py │ │ │ │ ├── feedback.py │ │ │ │ └── order.py │ │ │ └── restaurant │ │ │ │ ├── __init__.py │ │ │ │ ├── menu.py │ │ │ │ └── restaurant.py │ │ └── schemas │ │ │ ├── __init__.py │ │ │ ├── account │ │ │ ├── __init__.py │ │ │ ├── address.py │ │ │ └── profile.py │ │ │ ├── auth │ │ │ ├── __init__.py │ │ │ └── registration.py │ │ │ ├── common.py │ │ │ ├── driver │ │ │ ├── __init__.py │ │ │ ├── location.py │ │ │ ├── status.py │ │ │ └── vehicle.py │ │ │ ├── order │ │ │ ├── __init__.py │ │ │ ├── feedback.py │ │ │ └── order.py │ │ │ ├── restaurant │ │ │ ├── __init__.py │ │ │ ├── menu.py │ │ │ └── restaurant.py │ │ │ └── user.py │ │ ├── config │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── base.py │ │ ├── broker.py │ │ ├── cache.py │ │ ├── enums.py │ │ ├── password.py │ │ └── service.py │ │ ├── data_access │ │ ├── __init__.py │ │ ├── broker.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ └── lifecycle.py │ │ └── repository │ │ │ ├── __init__.py │ │ │ └── cache_repository.py │ │ ├── domain │ │ ├── __init__.py │ │ └── token_manager.py │ │ ├── main.py │ │ ├── middleware │ │ ├── __init__.py │ │ ├── authentication │ │ │ ├── __init__.py │ │ │ └── auth_middleware.py │ │ ├── builder.py │ │ ├── cors │ │ │ └── __init__.py │ │ ├── exception_handling │ │ │ ├── __init__.py │ │ │ └── handler.py │ │ ├── https_redirect │ │ │ └── __init__.py │ │ ├── logger │ │ │ ├── __init__.py │ │ │ └── handler.py │ │ ├── rate_limit │ │ │ ├── __init__.py │ │ │ └── handler.py │ │ ├── request_id │ │ │ ├── __init__.py │ │ │ └── handler.py │ │ └── timing │ │ │ ├── __init__.py │ │ │ └── handler.py │ │ ├── services │ │ ├── __init__.py │ │ ├── base.py │ │ ├── feedback.py │ │ ├── location.py │ │ ├── menu.py │ │ ├── order.py │ │ ├── restaurant.py │ │ ├── user.py │ │ └── vehicle.py │ │ └── utils │ │ ├── __init__.py │ │ └── exception.py ├── infra │ ├── admin │ │ ├── datasources │ │ │ ├── postgres.sh │ │ │ └── redis.sh │ │ └── docker-compose.yaml │ ├── docker-compose.yaml │ ├── mongo │ │ └── docker-compose.yaml │ ├── monitoring │ │ ├── docker-compose.yaml │ │ ├── grafana │ │ │ └── datasource.yaml │ │ └── prometheus │ │ │ └── prometheus.yaml │ ├── postgres │ │ └── docker-compose.yaml │ ├── rabbitmq │ │ └── docker-compose.yaml │ └── redis │ │ └── docker-compose.yaml ├── microservices │ ├── feedback │ │ ├── .dockerignore │ │ ├── .env │ │ ├── Dockerfile │ │ ├── pytest.ini │ │ ├── requirements.txt │ │ ├── src │ │ │ ├── __init__.py │ │ │ ├── application │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery.py │ │ │ │ ├── middleware.py │ │ │ │ └── order.py │ │ │ ├── config │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── db.py │ │ │ │ ├── enums.py │ │ │ │ └── service.py │ │ │ ├── data_access │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── db_repository.py │ │ │ │ └── events │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── lifecycle.py │ │ │ ├── domain │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery_rating.py │ │ │ │ ├── entities │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── base.py │ │ │ │ │ ├── delivery_rating.py │ │ │ │ │ └── order_rating.py │ │ │ │ └── order_rating.py │ │ │ ├── events.py │ │ │ ├── main.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery_rating.py │ │ │ │ └── order_rating.py │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ └── exception.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_doubles │ │ │ ├── __init__.py │ │ │ └── time.py │ │ │ └── unit_tests │ │ │ ├── __init__.py │ │ │ └── data_access │ │ │ ├── __init__.py │ │ │ └── repository │ │ │ └── __init__.py │ ├── location │ │ ├── .dockerignore │ │ ├── .env │ │ ├── Dockerfile │ │ ├── alembic.ini │ │ ├── migrations │ │ │ ├── README │ │ │ ├── env.py │ │ │ ├── script.py.mako │ │ │ └── versions │ │ │ │ └── 9fafa9afc18d_initial.py │ │ ├── pytest.ini │ │ ├── requirements.txt │ │ ├── src │ │ │ ├── __init__.py │ │ │ ├── application │ │ │ │ ├── __init__.py │ │ │ │ ├── driver.py │ │ │ │ ├── middleware.py │ │ │ │ └── tracking.py │ │ │ ├── config │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── cache.py │ │ │ │ ├── db.py │ │ │ │ ├── enums.py │ │ │ │ ├── hexagon.py │ │ │ │ ├── location.py │ │ │ │ ├── service.py │ │ │ │ └── status.py │ │ │ ├── data_access │ │ │ │ ├── __init__.py │ │ │ │ ├── broker.py │ │ │ │ ├── events │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── lifecycle.py │ │ │ │ ├── models │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── base.py │ │ │ │ │ └── driver_location.py │ │ │ │ └── repository │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── base.py │ │ │ │ │ ├── cache_repository.py │ │ │ │ │ └── db_repository.py │ │ │ ├── domain │ │ │ │ ├── __init__.py │ │ │ │ ├── driver.py │ │ │ │ ├── driver_location.py │ │ │ │ ├── geo_location.py │ │ │ │ └── hexagon.py │ │ │ ├── dto │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ └── location.py │ │ │ ├── events.py │ │ │ ├── main.py │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ └── exception.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_doubles │ │ │ ├── __init__.py │ │ │ ├── redis.py │ │ │ └── time.py │ │ │ └── unit_tests │ │ │ ├── __init__.py │ │ │ └── data_access │ │ │ ├── __init__.py │ │ │ └── repository │ │ │ ├── __init__.py │ │ │ └── test_cache_repository.py │ ├── order │ │ ├── .dockerignore │ │ ├── .env │ │ ├── Dockerfile │ │ ├── pytest.ini │ │ ├── requirements.txt │ │ ├── src │ │ │ ├── __init__.py │ │ │ ├── application │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery.py │ │ │ │ ├── middleware.py │ │ │ │ ├── order.py │ │ │ │ ├── order_status.py │ │ │ │ └── restaurant.py │ │ │ ├── config │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── cache.py │ │ │ │ ├── db.py │ │ │ │ ├── enums.py │ │ │ │ └── service.py │ │ │ ├── data_access │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── cache_repository.py │ │ │ │ ├── db_repository.py │ │ │ │ └── events │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── lifecycle.py │ │ │ ├── domain │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery.py │ │ │ │ ├── entities │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── base.py │ │ │ │ │ ├── delivery.py │ │ │ │ │ ├── order.py │ │ │ │ │ ├── order_item.py │ │ │ │ │ └── order_status.py │ │ │ │ ├── order.py │ │ │ │ ├── order_status.py │ │ │ │ └── restaurant.py │ │ │ ├── events.py │ │ │ ├── main.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── delivery_detail.py │ │ │ │ ├── order.py │ │ │ │ ├── order_item.py │ │ │ │ └── order_status.py │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ └── exception.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_doubles │ │ │ ├── __init__.py │ │ │ └── time.py │ │ │ └── unit_tests │ │ │ ├── __init__.py │ │ │ └── data_access │ │ │ ├── __init__.py │ │ │ └── repository │ │ │ └── __init__.py │ ├── restaurant │ │ ├── .dockerignore │ │ ├── .env │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── alembic.ini │ │ ├── migrations │ │ │ ├── README │ │ │ ├── env.py │ │ │ └── script.py.mako │ │ ├── requirements.txt │ │ ├── src │ │ │ ├── __init__.py │ │ │ ├── application │ │ │ │ ├── __init__.py │ │ │ │ ├── menu.py │ │ │ │ ├── middleware.py │ │ │ │ └── supplier.py │ │ │ ├── config │ │ │ │ ├── __init__.py │ │ │ │ ├── auth.py │ │ │ │ ├── base.py │ │ │ │ ├── broker.py │ │ │ │ ├── cache.py │ │ │ │ ├── db.py │ │ │ │ ├── enums.py │ │ │ │ └── service.py │ │ │ ├── data_access │ │ │ │ ├── __init__.py │ │ │ │ ├── broker.py │ │ │ │ ├── events │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── lifecycle.py │ │ │ │ └── repository │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── base.py │ │ │ │ │ ├── cache_repository.py │ │ │ │ │ └── db_repository.py │ │ │ ├── domain │ │ │ │ ├── __init__.py │ │ │ │ ├── menu.py │ │ │ │ └── restaurant.py │ │ │ ├── events.py │ │ │ ├── main.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── menu.py │ │ │ │ └── supplier.py │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ └── exception.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_event_sourcerer.py │ └── user │ │ ├── .coverage │ │ ├── .dockerignore │ │ ├── .env │ │ ├── Dockerfile │ │ ├── alembic.ini │ │ ├── migrations │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ ├── 1058a7c2c9f9_async_alembic_initial_tables.py │ │ │ ├── a227d3acc2ff_async_alembic_initial_tables.py │ │ │ ├── c457ed863290_async_alembic_initial_tables.py │ │ │ └── d9a4133406b7_async_alembic_initial_tables.py │ │ ├── pytest.ini │ │ ├── requirements.txt │ │ ├── src │ │ ├── __init__.py │ │ ├── application │ │ │ ├── __init__.py │ │ │ ├── address.py │ │ │ ├── middleware.py │ │ │ ├── profile.py │ │ │ └── vehicle.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── base.py │ │ │ ├── broker.py │ │ │ ├── cache.py │ │ │ ├── db.py │ │ │ ├── enums.py │ │ │ └── service.py │ │ ├── data_access │ │ │ ├── __init__.py │ │ │ ├── broker.py │ │ │ ├── events │ │ │ │ ├── __init__.py │ │ │ │ └── lifecycle.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ ├── address.py │ │ │ │ ├── base.py │ │ │ │ ├── profile.py │ │ │ │ └── vehicle.py │ │ │ └── repository │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── cache_repository.py │ │ │ │ └── db_repository.py │ │ ├── domain │ │ │ ├── __init__.py │ │ │ ├── assets │ │ │ │ ├── __init__.py │ │ │ │ ├── address.py │ │ │ │ └── vehicle.py │ │ │ ├── authentication.py │ │ │ ├── customer.py │ │ │ ├── driver.py │ │ │ ├── manager.py │ │ │ └── user.py │ │ ├── dto │ │ │ ├── __init__.py │ │ │ ├── address.py │ │ │ ├── base.py │ │ │ ├── profile.py │ │ │ └── vehicle.py │ │ ├── events.py │ │ ├── main.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ └── exception.py │ │ └── tests │ │ ├── __init__.py │ │ ├── component_test │ │ ├── __init__.py │ │ └── application │ │ │ ├── __init__.py │ │ │ ├── test_address.py │ │ │ └── test_profile.py │ │ ├── conftest.py │ │ ├── test_doubles │ │ ├── __init__.py │ │ ├── redis.py │ │ └── time.py │ │ └── unit_tests │ │ ├── __init__.py │ │ ├── data_access │ │ ├── __init__.py │ │ └── repository │ │ │ ├── __init__.py │ │ │ └── test_cache_repository.py │ │ └── domain │ │ ├── __init__.py │ │ ├── assets │ │ ├── __init__.py │ │ └── test_address.py │ │ ├── test_customer.py │ │ ├── test_driver.py │ │ └── test_user.py ├── start_services.sh └── stop_services.sh └── ui ├── .browserslistrc ├── .eslintrc.js ├── README.md ├── babel.config.js ├── images ├── McDonalds.png ├── Restaurant-Register.png ├── signin-image.jpg └── signup-image.jpg ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ ├── McDonalds.png │ ├── UserMainPage-image.jpg │ ├── UserMainPage-image.jpgZone.Identifier │ ├── background.jpg │ ├── default-avatar.png │ └── kebab.png └── index.html └── src ├── App.vue ├── assets ├── fonts │ ├── OFL.txt │ ├── README.txt │ ├── Vazirmatn-VariableFont_wght.ttf │ └── static │ │ ├── Vazirmatn-Black.ttf │ │ ├── Vazirmatn-Bold.ttf │ │ ├── Vazirmatn-ExtraBold.ttf │ │ ├── Vazirmatn-ExtraLight.ttf │ │ ├── Vazirmatn-Light.ttf │ │ ├── Vazirmatn-Medium.ttf │ │ ├── Vazirmatn-Regular.ttf │ │ ├── Vazirmatn-SemiBold.ttf │ │ └── Vazirmatn-Thin.ttf └── images │ ├── Restaurant-Register.png │ ├── destination-icon.png │ ├── location-logo.png │ ├── logo.jpg │ ├── logo_tmp.png │ └── restaurant-icon.png ├── axios.js ├── components ├── ChangeRestaurantInfo.vue ├── ChangeVehicleInfo.vue ├── CustomerChangeInfo.vue ├── CustomerMainPage.vue ├── DeliveryMainPage.vue ├── MenuPage.vue ├── RegisterRestaurantPage.vue ├── RegisterVehiclePage.vue ├── SignInComp.vue ├── SignUpComp.vue ├── SupplierMainPage.vue └── VerifyAccountPage.vue ├── main.js ├── router └── index.js ├── store └── index.js └── views ├── ChangeRestaurantInfo.vue ├── ChangeVehicleInfo.vue ├── CustomerChangeInfo.vue ├── CustomerMainPage.vue ├── DeliveryMainPage.vue ├── MenuPage.vue ├── RegisterRestaurantPage.vue ├── RegisterVehiclePage.vue ├── SignIn.vue ├── SignUp.vue ├── SupplierMainPage.vue └── VerifyAccountPage.vue /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .huskyrc.json 3 | out 4 | log.log 5 | **/node_modules 6 | *.pyc 7 | *.vsix 8 | envVars.txt 9 | **/.vscode/** 10 | **/testFiles/**/.cache/** 11 | *.noseids 12 | .nyc_output 13 | .vscode-test 14 | __pycache__ 15 | npm-debug.log 16 | **/.mypy_cache/** 17 | !yarn.lock 18 | coverage/ 19 | cucumber-report.json 20 | **/.vscode-test/** 21 | **/.vscode test/** 22 | **/.vscode-smoke/** 23 | **/.venv*/ 24 | port.txt 25 | precommit.hook 26 | python_files/lib/** 27 | python_files/get-pip.py 28 | debug_coverage*/** 29 | languageServer/** 30 | languageServer.*/** 31 | bin/** 32 | obj/** 33 | .pytest_cache 34 | tmp/** 35 | .python-version 36 | .vs/ 37 | test-results*.xml 38 | xunit-test-results.xml 39 | build/ci/performance/performance-results.json 40 | !build/ 41 | debug*.log 42 | debugpy*.log 43 | pydevd*.log 44 | nodeLanguageServer/** 45 | nodeLanguageServer.*/** 46 | dist/** 47 | # translation files 48 | *.xlf 49 | package.nls.*.json 50 | l10n/ 51 | python-env-tools/** -------------------------------------------------------------------------------- /backend/gateway/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/gateway/.env: -------------------------------------------------------------------------------- 1 | # Cache 2 | SERVICE_PORT=8000 3 | SERVICE_HOST=0.0.0.0 4 | ENVIRONMENT=test 5 | DEBUG=False 6 | LOG_LEVEL=WARNING 7 | TOKEN_SECRET_KEY = "SECRET" 8 | API_PREFIX="/api/v1" 9 | -------------------------------------------------------------------------------- /backend/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | WORKDIR /gateway 5 | 6 | EXPOSE 8000 7 | 8 | ENV ENVIRONMENT=dev 9 | 10 | RUN apt-get update && apt-get install -y \ 11 | git \ 12 | ca-certificates \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Upgrade pip 16 | RUN pip install --upgrade pip 17 | 18 | COPY ./requirements.txt /gateway/requirements.txt 19 | 20 | RUN pip install --no-cache-dir -r /gateway/requirements.txt 21 | 22 | RUN pip install prometheus-fastapi-instrumentator 23 | 24 | COPY ./ /gateway/ 25 | 26 | ENV PYTHONPATH=/gateway/src 27 | 28 | EXPOSE 8000 29 | 30 | CMD python /gateway/src/main.py 31 | -------------------------------------------------------------------------------- /backend/gateway/requirements.txt: -------------------------------------------------------------------------------- 1 | argon2_cffi 2 | asgi_lifespan 3 | asyncio 4 | bcrypt 5 | black 6 | colorama 7 | fastapi 8 | pydantic>=2.0.0,<3.0.0 9 | greenlet 10 | httpx 11 | isort 12 | loguru 13 | mypy 14 | passlib 15 | pathlib 16 | pre-commit 17 | pyotp 18 | pytest 19 | pytest-asyncio 20 | pytest-cov 21 | pytest-xdist 22 | python-decouple 23 | python-dotenv 24 | python-slugify 25 | slowapi 26 | redis 27 | aiocache 28 | pytz 29 | trio 30 | uvicorn 31 | uvloop 32 | starlette 33 | git+https://github.com/deepmancer/ftgo-utils.git 34 | git+https://github.com/deepmancer/aredis-client.git 35 | git+https://github.com/deepmancer/rabbitmq-rpc.git 36 | -------------------------------------------------------------------------------- /backend/gateway/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.GATEWAY.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/gateway/src/application/app.py: -------------------------------------------------------------------------------- 1 | from application.routes.account import profile_router 2 | from application.routes.auth import authentication_router 3 | from application.routes.customer import address_router 4 | from application.routes.driver import driver_status_router, driver_location_router, driver_vehicle_router 5 | from application.routes.order import feedback_router, order_location_router 6 | from application.routes.restaurant import restaurant_router, menu_router 7 | from fastapi import APIRouter 8 | 9 | 10 | def init_router() -> APIRouter: 11 | router = APIRouter() 12 | router.include_router(authentication_router) 13 | router.include_router(profile_router) 14 | router.include_router(address_router) 15 | router.include_router(driver_location_router) 16 | router.include_router(driver_status_router) 17 | router.include_router(driver_vehicle_router) 18 | router.include_router(restaurant_router) 19 | router.include_router(menu_router) 20 | router.include_router(feedback_router) 21 | router.include_router(order_location_router) 22 | return router 23 | -------------------------------------------------------------------------------- /backend/gateway/src/application/dependencies.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from fastapi import Depends, Request, status 3 | from typing import List, Union 4 | 5 | from ftgo_utils.errors import BaseError, ErrorCodes 6 | 7 | from application.schemas.user import UserStateSchema 8 | 9 | class AccessManager: 10 | def __init__(self, allowed_roles: List[Union[str, Enum]]) -> None: 11 | self.allowed_roles = [role.value if isinstance(role, Enum) else role for role in allowed_roles] 12 | 13 | def __call__(self, request: Request) -> None: 14 | if not hasattr(request.state, "user") or request.state.user is None: 15 | return 16 | 17 | user: UserStateSchema = request.state.user 18 | if user.role not in self.allowed_roles: 19 | raise BaseError( 20 | error_code=ErrorCodes.USER_PERMISSION_DENIED_ERROR, 21 | status_code=status.HTTP_403_FORBIDDEN, 22 | message=f"Access forbidden: Only roles of [{', '.join(self.allowed_roles)}] are allowed", 23 | issuer=self.__class__.__name__, 24 | ) 25 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | def get_logger(layer: str = LayerNames.GATEWAY.value): 6 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/account/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.account.profile import router as profile_router 2 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.auth.registration import router as authentication_router 2 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/customer/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.customer.address import router as address_router 2 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/driver/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.driver.location import router as driver_location_router 2 | from application.routes.driver.online_status import router as driver_status_router 3 | from application.routes.driver.vehicle import router as driver_vehicle_router 4 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/order/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.order.feedback import router as feedback_router 2 | from application.routes.order.order import router as order_location_router 3 | -------------------------------------------------------------------------------- /backend/gateway/src/application/routes/restaurant/__init__.py: -------------------------------------------------------------------------------- 1 | from application.routes.restaurant.restaurant import router as restaurant_router 2 | from application.routes.restaurant.menu import router as menu_router 3 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | def get_logger(layer: str = LayerNames.GATEWAY.value): 6 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/application/schemas/account/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/account/address.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import Field 4 | 5 | from ftgo_utils.schemas import ( 6 | uuid_field, 7 | AddressMixin, 8 | BaseSchema, 9 | ) 10 | 11 | 12 | class AddressesSchema(BaseSchema): 13 | addresses: list[AddressMixin] = Field(...) 14 | 15 | class AddressIdSchema(BaseSchema): 16 | address_id: str = uuid_field() 17 | 18 | class AddressIdPreferencySchema(AddressIdSchema): 19 | is_default: Optional[bool] = Field(False) 20 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/account/profile.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel 3 | 4 | from pydantic import Field 5 | 6 | from ftgo_utils.schemas import ( 7 | PhoneNumberMixin, RoleMixin, UserIdMixin, GenderMixin, UserInfoMixin, UserMixin, TokenMixin 8 | ) 9 | 10 | 11 | class RegistrationSchema(UserInfoMixin): 12 | password: str = Field(..., min_length=8, max_length=128) 13 | 14 | class UserAuthCodeSchema(UserIdMixin): 15 | auth_code: str = Field(..., min_length=1, max_length=10) 16 | 17 | class LoginSchema(PhoneNumberMixin, RoleMixin): 18 | password: str = Field(..., min_length=8, max_length=128) 19 | 20 | class ChangePasswordSchema(UserIdMixin): 21 | old_password: str = Field(..., min_length=8, max_length=30) 22 | new_password: str = Field(..., min_length=8, max_length=30) 23 | 24 | class UpdateProfileSchema(UserIdMixin, GenderMixin): 25 | first_name: Optional[str] = Field(None, min_length=1, max_length=50) 26 | last_name: Optional[str] = Field(None, min_length=1, max_length=50) 27 | 28 | class UserInfo(UserInfoMixin): 29 | role: str = Field(..., min_length=1, max_length=20) 30 | 31 | 32 | class UpdateUserRequest(BaseModel): 33 | first_name: str = Field(..., min_length=1, max_length=100) 34 | last_name: str = Field(..., min_length=1, max_length=100) -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from application.schemas.auth import * 2 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/auth/registration.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import Field 4 | 5 | from ftgo_utils.schemas import ( 6 | PhoneNumberMixin, RoleMixin, UserIdMixin, GenderMixin, UserInfoMixin, UserMixin, TokenMixin 7 | ) 8 | 9 | 10 | class RegistrationSchema(UserInfoMixin): 11 | #TODO fix max_length in ftgo_utils 12 | role: str = Field(..., min_length=1, max_length=20) 13 | password: str = Field(..., min_length=8, max_length=128) 14 | 15 | class UserAuthCodeSchema(UserIdMixin): 16 | auth_code: str = Field(..., min_length=1, max_length=10) 17 | 18 | class LoginSchema(PhoneNumberMixin, RoleMixin): 19 | # TODO fix max_length in ftgo_utils 20 | role: str = Field(..., min_length=1, max_length=20) 21 | password: str = Field(..., min_length=8, max_length=128) 22 | 23 | class ChangePasswordSchema(UserIdMixin): 24 | old_password: str = Field(..., min_length=8, max_length=30) 25 | new_password: str = Field(..., min_length=8, max_length=30) 26 | 27 | class UpdateProfileSchema(UserIdMixin, GenderMixin): 28 | first_name: Optional[str] = Field(None, min_length=1, max_length=50) 29 | last_name: Optional[str] = Field(None, min_length=1, max_length=50) 30 | 31 | class LoggedInUserSchema(UserMixin, TokenMixin): 32 | # TODO fix max_length in ftgo_utils 33 | role: str = Field(..., min_length=1, max_length=20) 34 | pass 35 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/common.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import Field 4 | 5 | from ftgo_utils.schemas import BaseSchema 6 | 7 | class EmptyResponse(BaseSchema): 8 | pass 9 | 10 | class SuccessResponse(BaseSchema): 11 | success: Optional[bool] = Field(True) 12 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/driver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/application/schemas/driver/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/driver/location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from pydantic import Field 4 | 5 | from ftgo_utils.schemas import LocationMixin, BaseSchema, LocationPointMixin 6 | 7 | class LocationsSchema(BaseSchema): 8 | locations: List[LocationMixin] = Field(..., min_items=1) 9 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/driver/status.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ftgo_utils.schemas import BaseSchema 4 | 5 | class DriverStatusSchema(BaseSchema): 6 | is_online: bool = Field(...) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/driver/vehicle.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel, Field 3 | from ftgo_utils.schemas import ( 4 | uuid_field, 5 | ) 6 | 7 | 8 | class RegisterVehicleRequest(BaseModel): 9 | plate_number: str = Field(..., min_length=1, max_length=100) 10 | license_number: str = Field(..., min_length=1, max_length=100) 11 | 12 | 13 | class RegisterVehicleResponse(BaseModel): 14 | vehicle_id: str = uuid_field() 15 | 16 | 17 | 18 | class GetVehicleInfoResponse(BaseModel): 19 | vehicle_id: str = uuid_field() 20 | driver_id: str = uuid_field() 21 | plate_number: str = Field(..., min_length=1, max_length=100) 22 | license_number: str = Field(..., min_length=1, max_length=100) 23 | 24 | 25 | class DeleteVehicleResponse(BaseModel): 26 | vehicle_id: str = uuid_field() -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/order/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/application/schemas/order/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/order/order.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | 3 | from pydantic import Field 4 | 5 | from ftgo_utils.schemas import ( 6 | uuid_field, 7 | AddressMixin, 8 | BaseSchema, 9 | ) 10 | 11 | 12 | class CreateOrderRequest(BaseSchema): 13 | restaurant_id: str = uuid_field() 14 | items: list[dict[str, Any]] 15 | 16 | 17 | class GetOrderHistoryRequest(BaseSchema): 18 | order_id: str = uuid_field() 19 | 20 | 21 | class GetOrderHistoryResponse(BaseSchema): 22 | customer_id: str = uuid_field() 23 | restaurant_id: str = uuid_field() 24 | total_amount: float = Field(..., gt=0) 25 | order_items: list[dict[str, Any]] 26 | status_history: list 27 | 28 | 29 | class UpdateOrderRequest(BaseSchema): 30 | items: list[dict[str, Any]] 31 | status_history: list 32 | total_amount: float = Field(..., gt=0) 33 | 34 | 35 | class ConfirmOrderRequest(BaseSchema): 36 | order_id: str = uuid_field() 37 | restaurant_id: str = uuid_field() 38 | 39 | 40 | class RejectOrderRequest(BaseSchema): 41 | order_id: str = uuid_field() 42 | restaurant_id: str = uuid_field() -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/restaurant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/application/schemas/restaurant/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/application/schemas/user.py: -------------------------------------------------------------------------------- 1 | from token import OP 2 | from typing import Optional 3 | 4 | from pydantic import Field 5 | 6 | from ftgo_utils.schemas import uuid_field, RoleMixin, PhoneNumberMixin, UserIdMixin 7 | 8 | class UserStateSchema(PhoneNumberMixin, RoleMixin, UserIdMixin): 9 | #TODO fix role max_length in fgto_utils 10 | role: str = Field(..., min_length=1, max_length=20) 11 | hashed_password: str = Field(..., min_length=1, max_length=512) 12 | token: Optional[str] = Field(None, min_length=1, max_length=1024) 13 | -------------------------------------------------------------------------------- /backend/gateway/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.auth import AuthConfig 3 | from config.cache import RedisConfig 4 | from config.enums import LayerNames 5 | from config.service import ServiceConfig 6 | -------------------------------------------------------------------------------- /backend/gateway/src/config/auth.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from config.base import BaseConfig, env_var 4 | 5 | 6 | class AuthConfig(BaseConfig): 7 | def __init__( 8 | self, 9 | algorithm: Optional[str] = None, 10 | access_token_expire_minutes: Optional[int] = None, 11 | token_location: Optional[list[str]] = None, 12 | excluded_urls: Optional[list[str]] = None, 13 | secret: Optional[str] = None, 14 | cache_key_prefix: Optional[str] = 'session_token_', 15 | ): 16 | self.algorithm = algorithm or env_var("AUTH_ALGORITHM", default="HS256") 17 | self.access_token_expire_minutes = access_token_expire_minutes or env_var("ACCESS_TOKEN_EXPIRE_MINUTES", default=30, cast_type=int) 18 | self.token_location = token_location if token_location is not None else env_var("TOKEN_LOCATION", default="headers", cast_type=lambda s: s.split(",")) 19 | self.excluded_urls = excluded_urls if excluded_urls is not None else env_var("EXCLUDED_URLS", default="", cast_type=lambda s: s.split(",")) 20 | self.secret = secret or env_var("TOKEN_SECRET_KEY", default="secret") 21 | self.cache_key_prefix = cache_key_prefix 22 | -------------------------------------------------------------------------------- /backend/gateway/src/config/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar, Callable 2 | 3 | from decouple import config, UndefinedValueError 4 | 5 | T = TypeVar('T') 6 | 7 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 8 | try: 9 | value = config(field_name, default=default) 10 | if value is None: 11 | return default 12 | return cast_type(value) 13 | except UndefinedValueError: 14 | return default 15 | except (TypeError, ValueError) as e: 16 | if cast_type is None: 17 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 18 | else: 19 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 20 | 21 | class BaseConfig(): 22 | 23 | @classmethod 24 | def load_environment(cls): 25 | env = config("ENVIRONMENT", default='test') 26 | return env 27 | 28 | def __repr__(self): 29 | class_name = self.__class__.__name__ 30 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 31 | return f'{class_name}({attributes})' 32 | 33 | def dict(self): 34 | return self.__dict__ 35 | -------------------------------------------------------------------------------- /backend/gateway/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5673, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/gateway/src/config/cache.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class RedisConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | db: int = None, 9 | default_ttl: int = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("REDIS_HOST", "localhost") 13 | self.port = port or env_var("REDIS_PORT", 6490, int) 14 | self.db = db or env_var("REDIS_DB", 0, int) 15 | self.default_ttl = default_ttl or env_var("REDIS_DEFAULT_TTL", 1200, int) 16 | self.password = password or env_var("REDIS_PASSWORD", "gateway_password") 17 | -------------------------------------------------------------------------------- /backend/gateway/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | GATEWAY = "gateway" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | MIDDLEWARE = "middleware" 9 | -------------------------------------------------------------------------------- /backend/gateway/src/config/password.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class PasswordConfig(BaseConfig): 4 | def __init__(self, schema: str = None, rounds: int = None): 5 | self.schema = schema or env_var("PASSWORD_SCHEMA", default="bcrypt") 6 | self.rounds = rounds or env_var("PASSWORD_ROUNDS", default=12, cast_type=int) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from config.base import BaseConfig, env_var 4 | 5 | 6 | class ServiceConfig(BaseConfig): 7 | def __init__( 8 | self, 9 | environment: str = None, 10 | api_prefix: str = None, 11 | simulator_api_prefix: str = None, 12 | service_host: str = None, 13 | service_port: int = None, 14 | log_level_name: str = None, 15 | debug: bool = None, 16 | ): 17 | self.environment = environment or env_var('ENVIRONMENT', default='test') 18 | self.api_prefix = api_prefix or env_var('API_PREFIX', default='/api/v1') 19 | self.simulator_api_prefix = simulator_api_prefix or env_var('SIMULATOR_API_PREFIX', default='/simulator/v1') 20 | self.service_host = service_host or env_var('SERVICE_HOST', default='0.0.0.0') 21 | self.service_port = service_port or env_var('SERVICE_PORT', default=8000, cast_type=int) 22 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 23 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 24 | self.debug = debug if debug is not None else env_var('DEBUG', default=True, cast_type=lambda s: s.lower() in ['true', '1']) 25 | -------------------------------------------------------------------------------- /backend/gateway/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/gateway/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/gateway/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/gateway/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from rabbitmq_rpc import RPCClient 5 | 6 | from data_access.repository.cache_repository import CacheRepository 7 | from data_access.broker import RPCBroker 8 | 9 | from config import BaseConfig 10 | from data_access import get_logger 11 | RPCClient 12 | async def setup() -> None: 13 | logger = get_logger() 14 | await CacheRepository.initialize() 15 | logger.info("Connected to Redis") 16 | await RPCBroker.initialize() 17 | logger.info("Connected to RabbitMQ") 18 | 19 | 20 | async def teardown() -> None: 21 | logger = get_logger() 22 | await CacheRepository.terminate() 23 | logger.info("Disconnected from Redis") 24 | await RPCBroker.terminate() 25 | logger.info("Disconnected from RabbitMQ") -------------------------------------------------------------------------------- /backend/gateway/src/data_access/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.repository.cache_repository import CacheRepository 2 | -------------------------------------------------------------------------------- /backend/gateway/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from config import ServiceConfig, LayerNames 2 | from ftgo_utils.logger import get_logger as _get_logger 3 | 4 | layer = LayerNames.DOMAIN.value 5 | 6 | def get_logger(layer: str = layer): 7 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 8 | -------------------------------------------------------------------------------- /backend/gateway/src/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from dotenv import load_dotenv 3 | from fastapi import FastAPI 4 | from ftgo_utils.logger import init_logging, get_logger 5 | from prometheus_fastapi_instrumentator import Instrumentator 6 | 7 | from application.app import init_router 8 | from config import ServiceConfig 9 | from data_access.events.lifecycle import setup, teardown 10 | from middleware.builder import MiddlewareBuilder 11 | 12 | load_dotenv() 13 | 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def lifespan(app: FastAPI): 18 | await setup() 19 | 20 | yield 21 | 22 | await teardown() 23 | 24 | app = FastAPI( 25 | title="Food Delivery Server", 26 | debug=service_config.debug, 27 | lifespan=lifespan, 28 | ) 29 | 30 | app.include_router(init_router(), prefix=service_config.api_prefix) 31 | Instrumentator().instrument(app).expose(app) 32 | middleware_builder = ( 33 | MiddlewareBuilder() 34 | .add_rate_limit() 35 | .add_authentication() 36 | .add_logger() 37 | .add_request_id() 38 | .add_exception_handling() 39 | .add_timing() 40 | .add_cors() 41 | ) 42 | 43 | middleware_builder.build(app=app) 44 | if __name__ == "__main__": 45 | load_dotenv() 46 | 47 | service_config = ServiceConfig() 48 | init_logging(level=service_config.log_level) 49 | get_logger().info("Running the Gateway Service") 50 | uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level=10, reload=True) -------------------------------------------------------------------------------- /backend/gateway/src/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from config import ServiceConfig, LayerNames 2 | from ftgo_utils.logger import get_logger as _get_logger 3 | 4 | layer = LayerNames.MIDDLEWARE.value 5 | 6 | def get_logger(layer: str = layer): 7 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 8 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from middleware.authentication.auth_middleware import JWTAuthenticationMiddleware 3 | 4 | def mount_middleware(app: FastAPI): 5 | app.add_middleware(JWTAuthenticationMiddleware) 6 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/cors/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | def mount_middleware(app: FastAPI): 6 | app.add_middleware( 7 | CORSMiddleware, 8 | allow_credentials=True, 9 | allow_origins=["*"], 10 | allow_methods=["*"], 11 | allow_headers=["*"], 12 | expose_headers=["*"], 13 | ) 14 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/exception_handling/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.exceptions import RequestValidationError 3 | 4 | from middleware.exception_handling.handler import validation_exception_handler 5 | 6 | 7 | def mount_middleware(app: FastAPI): 8 | app.add_exception_handler(RequestValidationError, validation_exception_handler) 9 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/exception_handling/handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from fastapi import Request, status 4 | from fastapi.encoders import jsonable_encoder 5 | from fastapi.exceptions import RequestValidationError 6 | from fastapi.responses import JSONResponse 7 | 8 | 9 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 10 | common_fields = { 11 | "path": request.url.path, 12 | "timestamp": int(time.time()), 13 | "method": request.method, 14 | } 15 | 16 | error_details = { 17 | **common_fields, 18 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 19 | "detail": exc.errors(), 20 | "body": exc.body 21 | } 22 | 23 | return JSONResponse( 24 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 25 | content=jsonable_encoder(error_details), 26 | ) 27 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/https_redirect/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware 3 | 4 | 5 | def mount_middleware(app: FastAPI): 6 | app.add_middleware(HTTPSRedirectMiddleware) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/logger/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from middleware.logger.handler import LoggingMiddleware 4 | 5 | def mount_middleware(app: FastAPI) -> None: 6 | app.add_middleware(LoggingMiddleware) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/logger/handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from middleware import get_logger 3 | from starlette.middleware.base import BaseHTTPMiddleware 4 | 5 | 6 | class LoggingMiddleware(BaseHTTPMiddleware): 7 | async def dispatch(self, request: Request, call_next): 8 | get_logger().info(f"API {request.url} with request_id {request.state.request_id} was called") 9 | return await call_next(request) 10 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/rate_limit/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from middleware.rate_limit.handler import ( 4 | RateLimiter, _rate_limit_exceeded_handler, RateLimitExceeded, 5 | ) 6 | 7 | def mount_middleware(app: FastAPI): 8 | app.state.limiter = RateLimiter() 9 | app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) 10 | app.add_middleware( 11 | RateLimiter.middleware_class(), 12 | **RateLimiter.middleware_kwargs(), 13 | ) 14 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/rate_limit/handler.py: -------------------------------------------------------------------------------- 1 | from slowapi import Limiter 2 | from slowapi.middleware import SlowAPIASGIMiddleware 3 | from slowapi.util import get_remote_address 4 | 5 | 6 | class RateLimiter: 7 | limiter = None 8 | 9 | def __new__(cls): 10 | if not hasattr(cls, 'instance'): 11 | cls.instance = super(RateLimiter, cls).__new__(cls) 12 | cls.limiter = Limiter(key_func=get_remote_address, default_limits=["20/minute"]) 13 | return cls.limiter 14 | 15 | @staticmethod 16 | def middleware_class(): 17 | return SlowAPIASGIMiddleware 18 | 19 | @staticmethod 20 | def middleware_kwargs(): 21 | return {} 22 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/request_id/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from middleware.request_id.handler import RequestUUIDMiddleware 4 | 5 | def mount_middleware(app: FastAPI) -> None: 6 | app.add_middleware(RequestUUIDMiddleware) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/request_id/handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from ftgo_utils.uuid_gen import uuid4 3 | from starlette.middleware.base import BaseHTTPMiddleware 4 | 5 | 6 | class RequestUUIDMiddleware(BaseHTTPMiddleware): 7 | async def dispatch(self, request: Request, call_next): 8 | request_id = str(uuid4()) 9 | request.state.request_id = request_id 10 | response = await call_next(request) 11 | response.headers["X-Request-ID"] = request_id 12 | return response 13 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/timing/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from middleware.timing.handler import ProcessTimeMiddleware 4 | 5 | def mount_middleware(app: FastAPI) -> None: 6 | app.add_middleware(ProcessTimeMiddleware) 7 | -------------------------------------------------------------------------------- /backend/gateway/src/middleware/timing/handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from fastapi import Request 4 | from starlette.middleware.base import BaseHTTPMiddleware 5 | 6 | 7 | class ProcessTimeMiddleware(BaseHTTPMiddleware): 8 | async def dispatch(self, request: Request, call_next): 9 | start_time = time.time() 10 | response = await call_next(request) 11 | process_time = time.time() - start_time 12 | if response is not None: 13 | response.headers["X-Process-Time"] = str(process_time) 14 | return response 15 | -------------------------------------------------------------------------------- /backend/gateway/src/services/__init__.py: -------------------------------------------------------------------------------- 1 | from services.user import UserService 2 | from services.location import LocationService 3 | from services.vehicle import VehicleService 4 | from services.feedback import FeedbackService 5 | from services.order import OrderService 6 | -------------------------------------------------------------------------------- /backend/gateway/src/services/base.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from config import LayerNames, BaseConfig 4 | from data_access.broker import RPCBroker 5 | from ftgo_utils.enums import ResponseStatus 6 | from ftgo_utils.errors import ErrorCodes 7 | from ftgo_utils.logger import get_logger 8 | 9 | logger = get_logger(layer=LayerNames.MESSAGE_BROKER.value, environment=BaseConfig.load_environment()) 10 | 11 | class Microservice: 12 | _service_name = '' 13 | 14 | @classmethod 15 | async def _call_rpc(cls, event_name: str, data: Dict[str, Any], **kwargs) -> Dict[str, Any]: 16 | try: 17 | rpc_client = RPCBroker.get_client() 18 | response = await rpc_client.call(event_name, data=data, **kwargs) 19 | if response.get('status') not in [status.value for status in ResponseStatus]: 20 | raise ValueError(f"Invalid response status: {response}") 21 | return response 22 | except Exception as e: 23 | logger.error(f"Exception at calling event: {event_name} in service: {cls._service_name}: {e}") 24 | return { 25 | "response": ResponseStatus.ERROR.value, 26 | "error_code": ErrorCodes.UNKNOWN_ERROR.value, 27 | } 28 | -------------------------------------------------------------------------------- /backend/gateway/src/services/location.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from services.base import Microservice 4 | 5 | 6 | class LocationService(Microservice): 7 | _service_name = 'location' 8 | 9 | @classmethod 10 | async def submit_location(cls, data: Dict) -> Dict: 11 | return await cls._call_rpc('driver.location.submit', data=data) 12 | 13 | @classmethod 14 | async def change_status_online(cls, data: Dict) -> Dict: 15 | return await cls._call_rpc('driver.status.online', data=data) 16 | 17 | @classmethod 18 | async def change_status_offline(cls, data: Dict) -> Dict: 19 | return await cls._call_rpc('driver.status.offline', data=data) 20 | 21 | @classmethod 22 | async def get_nearest_drivers(cls, data: Dict) -> Dict: 23 | return await cls._call_rpc('location.drivers.get_nearest', data=data) 24 | 25 | @classmethod 26 | async def get_last_location(cls, data: Dict) -> Dict: 27 | return await cls._call_rpc('driver.location.get', data=data) 28 | 29 | @classmethod 30 | async def get_driver_status(cls, data: Dict) -> Dict: 31 | return await cls._call_rpc('driver.status.get', data=data) 32 | -------------------------------------------------------------------------------- /backend/gateway/src/services/menu.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from services.base import Microservice 4 | 5 | 6 | class MenuService(Microservice): 7 | _service_name = 'menu' 8 | 9 | @classmethod 10 | async def add_item(cls, data: Dict) -> Dict: 11 | return await cls._call_rpc('restaurant.menu.add_item', data=data) 12 | 13 | @classmethod 14 | async def get_item_info(cls, data: Dict) -> Dict: 15 | return await cls._call_rpc('restaurant.menu.get_item_info', data=data) 16 | 17 | @classmethod 18 | async def update_item(cls, data: Dict) -> Dict: 19 | return await cls._call_rpc('restaurant.menu.update_item', data=data) 20 | 21 | @classmethod 22 | async def delete_item(cls, data: Dict) -> Dict: 23 | return await cls._call_rpc('restaurant.menu.delete_item', data=data) 24 | 25 | @classmethod 26 | async def get_all_menu_item(cls, data: Dict) -> Dict: 27 | return await cls._call_rpc('restaurant.menu.get_all_menu_item', data=data) 28 | -------------------------------------------------------------------------------- /backend/gateway/src/services/order.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from services.base import Microservice 4 | 5 | 6 | class OrderService(Microservice): 7 | _service_name = 'order' 8 | 9 | @classmethod 10 | async def get_order_history(cls, data: Dict) -> Dict: 11 | return await cls._call_rpc('order.history', data=data) 12 | 13 | @classmethod 14 | async def create_order(cls, data: Dict) -> Dict: 15 | return await cls._call_rpc('order.create', data=data) 16 | 17 | @classmethod 18 | async def update_order(cls, data: Dict) -> Dict: 19 | return await cls._call_rpc('order.update', data=data) 20 | 21 | @classmethod 22 | async def restaurant_confirm(cls, data: Dict) -> Dict: 23 | return await cls._call_rpc('order.restaurant.confirm', data=data) 24 | 25 | @classmethod 26 | async def restaurant_reject(cls, data: Dict) -> Dict: 27 | return await cls._call_rpc('order.restaurant.reject', data=data) -------------------------------------------------------------------------------- /backend/gateway/src/services/restaurant.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from services.base import Microservice 4 | 5 | 6 | class RestaurantService(Microservice): 7 | _service_name = 'restaurant' 8 | 9 | @classmethod 10 | async def register(cls, data: Dict) -> Dict: 11 | return await cls._call_rpc('restaurant.supplier.register', data=data) 12 | 13 | @classmethod 14 | async def get_restaurant_info(cls, data: Dict) -> Dict: 15 | return await cls._call_rpc('restaurant.supplier.get_restaurant_info', data=data) 16 | 17 | @classmethod 18 | async def get_all_restaurant_info(cls, data: Dict) -> Dict: 19 | return await cls._call_rpc('restaurant.supplier.get_all_restaurant_info', data=data) 20 | 21 | @classmethod 22 | async def get_supplier_restaurant_info(cls, data: Dict) -> Dict: 23 | return await cls._call_rpc('restaurant.supplier.get_supplier_restaurant_info', data=data) 24 | 25 | @classmethod 26 | async def update_information(cls, data: Dict) -> Dict: 27 | return await cls._call_rpc('restaurant.supplier.update_information', data=data) 28 | 29 | @classmethod 30 | async def delete_restaurant(cls, data: Dict) -> Dict: 31 | return await cls._call_rpc('restaurant.supplier.delete_restaurant', data=data) 32 | 33 | -------------------------------------------------------------------------------- /backend/gateway/src/services/vehicle.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from services.base import Microservice 4 | 5 | 6 | class VehicleService(Microservice): 7 | _service_name = 'vehicle' 8 | 9 | @classmethod 10 | async def register(cls, data: Dict) -> Dict: 11 | return await cls._call_rpc('driver.vehicle.register', data=data) 12 | 13 | @classmethod 14 | async def get_info(cls, data: Dict) -> Dict: 15 | return await cls._call_rpc('driver.vehicle.get_info', data=data) 16 | 17 | @classmethod 18 | async def delete(cls, data: Dict) -> Dict: 19 | return await cls._call_rpc('driver.vehicle.delete', data=data) 20 | 21 | -------------------------------------------------------------------------------- /backend/gateway/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/gateway/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/infra/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - ./postgres/docker-compose.yaml 3 | - ./redis/docker-compose.yaml 4 | - ./rabbitmq/docker-compose.yaml 5 | - ./mongo/docker-compose.yaml 6 | - ./admin/docker-compose.yaml 7 | - ./monitoring/docker-compose.yaml 8 | -------------------------------------------------------------------------------- /backend/infra/mongo/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | order_mongo: 3 | image: mongo:latest 4 | container_name: order_mongo 5 | restart: unless-stopped 6 | environment: 7 | MONGO_INITDB_ROOT_USERNAME: order_user 8 | MONGO_INITDB_ROOT_PASSWORD: order_password 9 | MONGO_INITDB_DATABASE: order_database 10 | ports: 11 | - "7017:27017" 12 | volumes: 13 | - order_mongo_data:/data/db 14 | networks: 15 | - backend-network 16 | 17 | feedback_mongo: 18 | image: mongo:latest 19 | container_name: feedback_mongo 20 | restart: unless-stopped 21 | environment: 22 | MONGO_INITDB_ROOT_USERNAME: feedback_user 23 | MONGO_INITDB_ROOT_PASSWORD: feedback_password 24 | MONGO_INITDB_DATABASE: feedback_database 25 | ports: 26 | - "7018:27017" 27 | volumes: 28 | - feedback_mongo_data:/data/db 29 | networks: 30 | - backend-network 31 | 32 | volumes: 33 | order_mongo_data: 34 | feedback_mongo_data: 35 | 36 | networks: 37 | backend-network: 38 | external: true 39 | -------------------------------------------------------------------------------- /backend/infra/monitoring/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | prometheus: 3 | image: prom/prometheus 4 | container_name: prometheus 5 | command: 6 | - '--config.file=/etc/prometheus/prometheus.yaml' 7 | ports: 8 | - 9090:9090 9 | restart: unless-stopped 10 | volumes: 11 | - ./prometheus:/etc/prometheus 12 | - prometheus_data:/prometheus 13 | networks: 14 | - backend-network 15 | 16 | grafana: 17 | image: grafana/grafana 18 | container_name: grafana 19 | ports: 20 | - 3000:3000 21 | restart: unless-stopped 22 | environment: 23 | - GF_SECURITY_ADMIN_USER=admin 24 | - GF_SECURITY_ADMIN_PASSWORD=grafana 25 | volumes: 26 | - ./grafana:/etc/grafana/provisioning/datasources 27 | networks: 28 | - backend-network 29 | 30 | volumes: 31 | prometheus_data: 32 | 33 | networks: 34 | backend-network: 35 | external: true 36 | -------------------------------------------------------------------------------- /backend/infra/monitoring/grafana/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | url: http://prometheus:9090 7 | isDefault: true 8 | access: proxy 9 | editable: true 10 | -------------------------------------------------------------------------------- /backend/infra/monitoring/prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_timeout: 10s 4 | evaluation_interval: 15s 5 | alerting: 6 | alertmanagers: 7 | - static_configs: 8 | - targets: [] 9 | scheme: http 10 | timeout: 10s 11 | api_version: v1 12 | scrape_configs: 13 | - job_name: prometheus 14 | honor_timestamps: true 15 | scrape_interval: 15s 16 | scrape_timeout: 10s 17 | metrics_path: /metrics 18 | scheme: http 19 | static_configs: 20 | - targets: 21 | - localhost:9090 22 | -------------------------------------------------------------------------------- /backend/infra/postgres/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | user_postgres: 3 | image: postgres:16.3 4 | container_name: "user_postgres" 5 | hostname: "user_postgres" 6 | environment: 7 | - POSTGRES_USER=user_user 8 | - POSTGRES_PASSWORD=user_password 9 | - POSTGRES_DB=user_database 10 | ports: 11 | - "5438:5432" 12 | volumes: 13 | - user_postgres_data:/var/lib/postgresql/data 14 | networks: 15 | - backend-network 16 | 17 | restaurant_postgres: 18 | image: postgres:16.3 19 | container_name: "restaurant_postgres" 20 | hostname: "restaurant_postgres" 21 | environment: 22 | - POSTGRES_USER=restaurant_user 23 | - POSTGRES_PASSWORD=restaurant_password 24 | - POSTGRES_DB=restaurant_database 25 | ports: 26 | - "5440:5432" 27 | volumes: 28 | - restaurant_postgres_data:/var/lib/postgresql/data 29 | networks: 30 | - backend-network 31 | 32 | location_postgres: 33 | image: postgres:16.3 34 | container_name: "location_postgres" 35 | hostname: "location_postgres" 36 | environment: 37 | - POSTGRES_USER=location_user 38 | - POSTGRES_PASSWORD=location_password 39 | - POSTGRES_DB=location_database 40 | ports: 41 | - "5439:5432" 42 | volumes: 43 | - location_postgres_data:/var/lib/postgresql/data 44 | networks: 45 | - backend-network 46 | 47 | volumes: 48 | user_postgres_data: 49 | restaurant_postgres_data: 50 | location_postgres_data: 51 | 52 | networks: 53 | backend-network: 54 | external: true 55 | -------------------------------------------------------------------------------- /backend/infra/rabbitmq/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | message_broker: 3 | image: rabbitmq:3-management 4 | container_name: "message_broker" 5 | hostname: "rabbitmq" 6 | restart: always 7 | environment: 8 | - RABBITMQ_DEFAULT_USER=rabbitmq_user 9 | - RABBITMQ_DEFAULT_PASS=rabbitmq_password 10 | - RABBITMQ_DEFAULT_VHOST=/ 11 | ports: 12 | - "15673:15672" 13 | - "5673:5672" 14 | volumes: 15 | - message_broker_data:/var/lib/rabbitmq 16 | networks: 17 | - backend-network 18 | 19 | volumes: 20 | message_broker_data: 21 | 22 | networks: 23 | backend-network: 24 | external: true 25 | -------------------------------------------------------------------------------- /backend/microservices/feedback/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/microservices/feedback/.env: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | LOG_LEVEL=INFO 3 | -------------------------------------------------------------------------------- /backend/microservices/feedback/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 6 | ENV ENVIRONMENT=dev 7 | 8 | # Upgrade pip and install Python dependencies 9 | #RUN apt-get update 10 | #RUN apt-get install -y git 11 | RUN apt-get update && \ 12 | apt-get install -y git ca-certificates && \ 13 | apt-get clean && \ 14 | rm -rf /var/lib/apt/lists/* 15 | # Upgrade pip 16 | RUN pip install --upgrade pip 17 | 18 | # Copy requirements.txt before other files to leverage Docker cache 19 | COPY ./requirements.txt /tmp/requirements.txt 20 | 21 | 22 | RUN pip install -r /tmp/requirements.txt --no-cache-dir --force-reinstall 23 | RUN pip install --upgrade --no-deps git+https://github.com/deepmancer/aredis-client.git 24 | # Copy the application code to the container 25 | COPY . /feedback 26 | 27 | # Set the working directory 28 | WORKDIR /feedback 29 | 30 | # Set the PYTHONPATH environment variable 31 | ENV PYTHONPATH=/feedback/src 32 | 33 | # RUN python -m pytest -v 34 | # Copy the Alembic configuration file 35 | # COPY alembic.ini /feedback/alembic.ini 36 | 37 | # Run Alembic migrations and start the application 38 | CMD python -u src/main.py 39 | -------------------------------------------------------------------------------- /backend/microservices/feedback/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = 3 | test_*.py 4 | *_test.py 5 | 6 | pythonpath = src tests 7 | 8 | testpaths = 9 | tests 10 | -------------------------------------------------------------------------------- /backend/microservices/feedback/requirements.txt: -------------------------------------------------------------------------------- 1 | asgi_lifespan 2 | asyncio 3 | beanie 4 | black 5 | dataclasses 6 | docker 7 | freezegun 8 | greenlet 9 | loguru 10 | motor 11 | pymongo 12 | pydantic>2.0 13 | python-decouple 14 | python-dotenv 15 | pytest 16 | pytest-asyncio 17 | pytest-cov 18 | pytz 19 | trio 20 | uvloop 21 | 22 | git+https://github.com/deepmancer/aredis-client.git 23 | git+https://github.com/deepmancer/ftgo-utils.git 24 | git+https://github.com/deepmancer/mongo-motors.git 25 | git+https://github.com/deepmancer/rabbitmq-rpc.git -------------------------------------------------------------------------------- /backend/microservices/feedback/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/src/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.APP.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/application/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any, Dict 2 | from functools import wraps 3 | 4 | from config import LayerNames, BaseConfig 5 | from application import get_logger 6 | 7 | from ftgo_utils.enums import ResponseStatus 8 | from ftgo_utils.errors import ErrorCodes, BaseError, ErrorCategories 9 | 10 | logger = get_logger() 11 | 12 | def event_middleware(event_name: str, func: Callable) -> Callable: 13 | @wraps(func) 14 | async def wrapper(*args, **kwargs) -> Dict[str, Any]: 15 | try: 16 | logger.info(f"event: {event_name} is called") 17 | result = await func(*args, **kwargs) 18 | if not isinstance(result, dict) or result is None: 19 | logger.warning(f"Expected result to be a dict, got {type(result)} instead.") 20 | result = {} 21 | 22 | result['status'] = ResponseStatus.SUCCESS.value 23 | return result 24 | 25 | except BaseError as e: 26 | logger.exception(f"Error in {event_name}: {e.error_code.value}", payload=e.to_dict()) 27 | error_code = e.error_code 28 | if error_code.category != ErrorCategories.BUSINESS_LOGIC_ERROR: 29 | error_code = ErrorCodes.UNKNOWN_ERROR 30 | return { 31 | "status": ResponseStatus.FAILURE.value, 32 | "error_code": error_code.value, 33 | } 34 | 35 | except Exception as e: 36 | logger.exception(f"Error in {event_name}: {ErrorCodes.UNKNOWN_ERROR.value}", payload={"error": str(e)}) 37 | return { 38 | "status": ResponseStatus.ERROR.value, 39 | "error_code": ErrorCodes.UNKNOWN_ERROR.value, 40 | } 41 | 42 | return wrapper 43 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.service import ServiceConfig 3 | from config.db import MongoConfig 4 | from config.enums import LayerNames 5 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, TypeVar, Callable 3 | from decouple import config, UndefinedValueError 4 | 5 | T = TypeVar('T') 6 | 7 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 8 | try: 9 | value = config(field_name, default=default) 10 | if value is None: 11 | return default 12 | return cast_type(value) 13 | except UndefinedValueError: 14 | return default 15 | except (TypeError, ValueError) as e: 16 | if cast_type is None: 17 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 18 | else: 19 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 20 | 21 | class BaseConfig(): 22 | @classmethod 23 | def load_environment(cls): 24 | env = config("ENVIRONMENT", default='test') 25 | return env 26 | 27 | def __repr__(self): 28 | class_name = self.__class__.__name__ 29 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 30 | return f'{class_name}({attributes})' 31 | 32 | def dict(self): 33 | return self.__dict__ 34 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5672, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/db.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class MongoConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | database: str = None, 9 | username: str = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("MONGO_HOST", default="localhost") 13 | self.port = port or env_var("MONGO_PORT", default=7018, cast_type=int) 14 | self.database = database or env_var("MONGO_DATABASE", default="feedback_database") 15 | self.username = username or env_var("MONGO_USERNAME", default="feedback_user") 16 | self.password = password or env_var("MONGO_PASSWORD", default="feedback_password") 17 | 18 | @property 19 | def url(self): 20 | return f"mongodb://{self.username}:{self.password}@{self.host}:{self.port}" 21 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | APP = "app" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import BaseConfig, env_var 3 | 4 | class ServiceConfig(BaseConfig): 5 | def __init__( 6 | self, 7 | environment: str = None, 8 | log_level_name: str = None, 9 | ): 10 | self.environment = environment or env_var('ENVIRONMENT', default='test') 11 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 12 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 13 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/data_access/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class BaseRepository: 5 | _data_access = None 6 | 7 | @classmethod 8 | async def initialize(cls) -> None: 9 | raise NotImplementedError 10 | 11 | @classmethod 12 | async def terminate(cls) -> None: 13 | if cls._data_access: 14 | await cls._data_access.disconnect() 15 | cls._data_access = None 16 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/data_access/db_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from beanie import init_beanie 3 | from mongo_motors import AsyncMongo 4 | 5 | from ftgo_utils.errors import ErrorCodes 6 | 7 | from config import MongoConfig 8 | from data_access import get_logger 9 | from data_access.base import BaseRepository 10 | from models import OrderRating, DeliveryRating 11 | from utils import handle_exception 12 | 13 | class DatabaseRepository(BaseRepository): 14 | _data_access: Optional[AsyncMongo] = None 15 | 16 | @classmethod 17 | async def initialize(cls) -> None: 18 | db_config = MongoConfig() 19 | try: 20 | mongo_data_access = await AsyncMongo.create( 21 | host=db_config.host, 22 | port=db_config.port, 23 | database=db_config.database, 24 | username=db_config.username, 25 | password=db_config.password, 26 | ) 27 | await init_beanie( 28 | database=mongo_data_access.get_database(), 29 | document_models=[OrderRating, DeliveryRating], 30 | ) 31 | cls._data_access = mongo_data_access 32 | 33 | except Exception as e: 34 | payload = db_config.dict() 35 | get_logger().error(ErrorCodes.DB_CONNECTION_ERROR.value, payload=payload) 36 | await handle_exception(e=e, error_code=ErrorCodes.DB_CONNECTION_ERROR, payload=payload) 37 | 38 | @classmethod 39 | async def terminate(cls) -> None: 40 | if cls._data_access: 41 | await cls._data_access.disconnect() 42 | cls._data_access = None 43 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from config import BaseConfig 5 | from data_access import get_logger 6 | from data_access.broker import RPCBroker 7 | from data_access.db_repository import DatabaseRepository 8 | 9 | 10 | async def setup() -> None: 11 | logger = get_logger() 12 | await DatabaseRepository.initialize() 13 | logger.info("Connected to MongoDB") 14 | await RPCBroker.initialize(asyncio.get_event_loop()) 15 | logger.info("Connected to RabbitMQ") 16 | 17 | 18 | async def teardown() -> None: 19 | logger = get_logger() 20 | await DatabaseRepository.terminate() 21 | logger.info("Disconnected from MongoDB") 22 | await RPCBroker.terminate() 23 | logger.info("Disconnected from RabbitMQ") 24 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DOMAIN.value 6 | 7 | def get_logger(layer: str =layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from domain.entities.delivery_rating import DeliveryRating 2 | from domain.entities.order_rating import OrderRating 3 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/domain/entities/base.py: -------------------------------------------------------------------------------- 1 | from pydoc import doc 2 | from beanie import Document 3 | from ftgo_utils.errors import ErrorCodes 4 | from typing import Any, Dict, Optional 5 | 6 | 7 | class BaseEntity: 8 | document_cls = None 9 | 10 | def __init__(self, document: Document): 11 | self.document: Document = document 12 | 13 | @classmethod 14 | def create(cls, **kwargs) -> "BaseEntity": 15 | raise NotImplementedError("The 'create' method must be implemented in the subclass.") 16 | 17 | @classmethod 18 | def build_query(cls, **kwargs) -> Dict[str, Any]: 19 | query = {} 20 | for key, value in kwargs.items(): 21 | if value is not None: 22 | query[key] = value 23 | return query 24 | 25 | @classmethod 26 | async def fetch_document(cls, **kwargs) -> Optional[Document]: 27 | query = cls.build_query(**kwargs) 28 | document = await cls.document_cls.find(**query).first_or_none() 29 | return document 30 | 31 | @classmethod 32 | async def load(cls, **kwargs) -> Optional["BaseEntity"]: 33 | raise NotImplementedError("The 'save' method must be implemented in the subclass.") 34 | 35 | async def save(self): 36 | raise NotImplementedError("The 'save' method must be implemented in the subclass.") 37 | 38 | async def delete(self): 39 | raise NotImplementedError("The 'delete' method must be implemented in the subclass.") 40 | 41 | async def update(self, **kwargs): 42 | raise NotImplementedError("The 'update' method must be implemented in the subclass.") 43 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/domain/entities/delivery_rating.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from domain.entities.base import BaseEntity 3 | from models.delivery_rating import DeliveryRating as DeliveryRatingDocument 4 | from typing import Optional 5 | class DeliveryRating(BaseEntity): 6 | document_cls = DeliveryRatingDocument 7 | 8 | def __init__(self, document: DeliveryRatingDocument): 9 | super().__init__(document) 10 | 11 | @classmethod 12 | def create(cls, delivery_id: str, order_id: str, customer_id: str, rating: int, feedback: Optional[str] = None, driver_id: Optional[str] = None) -> "DeliveryRating": 13 | delivery_rating_doc = DeliveryRatingDocument( 14 | delivery_id=delivery_id, 15 | order_id=order_id, 16 | customer_id=customer_id, 17 | driver_id=driver_id, 18 | rating=rating, 19 | feedback=feedback, 20 | created_at=datetime.utcnow(), 21 | updated_at=datetime.utcnow(), 22 | ) 23 | return cls(document=delivery_rating_doc) 24 | 25 | async def save(self): 26 | await super().save() 27 | 28 | async def update_rating(self, rating: int, feedback: Optional[str] = None): 29 | await self.update(rating=rating, feedback=feedback, updated_at=datetime.utcnow()) 30 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/domain/entities/order_rating.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from domain.entities.base import BaseEntity 3 | from models.order_rating import OrderRating as OrderRatingDocument 4 | from typing import Optional 5 | 6 | class OrderRating(BaseEntity): 7 | document_cls = OrderRatingDocument 8 | 9 | def __init__(self, document: OrderRatingDocument): 10 | super().__init__(document) 11 | 12 | @classmethod 13 | def create(cls, order_id: str, customer_id: str, rating: int, feedback: Optional[str] = None) -> "OrderRating": 14 | order_rating_doc = OrderRatingDocument( 15 | order_id=order_id, 16 | customer_id=customer_id, 17 | rating=rating, 18 | feedback=feedback, 19 | created_at=datetime.utcnow(), 20 | updated_at=datetime.utcnow(), 21 | ) 22 | return cls(document=order_rating_doc) 23 | 24 | async def save(self): 25 | await super().save() 26 | 27 | async def update_rating(self, rating: int, feedback: Optional[str] = None): 28 | await self.update(rating=rating, feedback=feedback, updated_at=datetime.utcnow()) 29 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvloop 3 | from dotenv import load_dotenv 4 | 5 | from ftgo_utils.logger import init_logging 6 | 7 | from config import ServiceConfig 8 | from data_access.events.lifecycle import setup, teardown 9 | from events import register_events 10 | 11 | load_dotenv() 12 | 13 | async def setup_env(): 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def startup_event(): 18 | await setup_env() 19 | await setup() 20 | await asyncio.sleep(1) 21 | await register_events() 22 | await asyncio.Future() 23 | 24 | async def shutdown_event(): 25 | await teardown() 26 | 27 | if __name__ == '__main__': 28 | uvloop.install() 29 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 30 | loop = asyncio.get_event_loop() 31 | 32 | try: 33 | loop.run_until_complete(startup_event()) 34 | finally: 35 | loop.run_until_complete(shutdown_event()) 36 | loop.close() 37 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/models/__init__.py: -------------------------------------------------------------------------------- 1 | from models.delivery_rating import DeliveryRating 2 | from models.order_rating import OrderRating 3 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/models/delivery_rating.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from beanie import Document 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | 9 | 10 | class DeliveryRating(Document): 11 | delivery_id: str = Field(..., index=True) 12 | order_id: str = Field(..., index=True) 13 | customer_id: str = Field(..., index=True) 14 | driver_id: Optional[str] = Field(None, index=True) 15 | rating: int = Field(..., ge=1, le=5) # Rating from 1 to 5 16 | feedback: Optional[str] = Field(None, max_length=1000) 17 | 18 | created_at: datetime = Field(default_factory=datetime.utcnow) 19 | updated_at: datetime = Field(default_factory=datetime.utcnow) 20 | 21 | class Settings: 22 | name = "delivery_ratings" 23 | indexes = [ 24 | IndexModel([("delivery_id", pymongo.ASCENDING)], name="delivery_rating_delivery_id_index"), 25 | IndexModel([("order_id", pymongo.ASCENDING)], name="delivery_rating_order_id_index"), 26 | IndexModel([("customer_id", pymongo.ASCENDING)], name="delivery_rating_customer_id_index"), 27 | IndexModel([("driver_id", pymongo.ASCENDING)], name="delivery_rating_driver_id_index"), 28 | ] 29 | use_state_management = True 30 | validate_on_save = True 31 | 32 | @classmethod 33 | async def before_insert(cls, instance): 34 | instance.created_at = datetime.utcnow() 35 | instance.updated_at = datetime.utcnow() 36 | 37 | @classmethod 38 | async def before_replace(cls, instance): 39 | instance.updated_at = datetime.utcnow() 40 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/models/order_rating.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from beanie import Document 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | 9 | class OrderRating(Document): 10 | order_id: str = Field(..., index=True) 11 | customer_id: str = Field(..., index=True) 12 | rating: int = Field(..., ge=1, le=5) # Rating from 1 to 5 13 | feedback: Optional[str] = Field(None, max_length=1000) 14 | 15 | created_at: datetime = Field(default_factory=datetime.utcnow) 16 | updated_at: datetime = Field(default_factory=datetime.utcnow) 17 | 18 | class Settings: 19 | name = "order_ratings" 20 | indexes = [ 21 | IndexModel([("order_id", pymongo.ASCENDING)], name="order_rating_order_id_index"), 22 | IndexModel([("customer_id", pymongo.ASCENDING)], name="order_rating_customer_id_index"), 23 | ] 24 | use_state_management = True 25 | validate_on_save = True 26 | 27 | @classmethod 28 | async def before_insert(cls, instance): 29 | instance.created_at = datetime.utcnow() 30 | instance.updated_at = datetime.utcnow() 31 | 32 | @classmethod 33 | async def before_replace(cls, instance): 34 | instance.updated_at = datetime.utcnow() 35 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/microservices/feedback/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import pytest_asyncio 4 | 5 | from test_doubles.time import TimeProvider 6 | 7 | MOCKED_TIMESTAMP = 1704067200 # 2024, January 1 8 | 9 | @pytest.fixture(scope='function') 10 | def time_machine(): 11 | provider = TimeProvider(timestamp=MOCKED_TIMESTAMP) 12 | provider.start() 13 | yield provider 14 | provider.stop() 15 | 16 | -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/test_doubles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/tests/test_doubles/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/test_doubles/time.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from typing import Optional 4 | 5 | from freezegun import freeze_time 6 | 7 | 8 | class TimeProvider: 9 | def __init__(self, timestamp: float) -> None: 10 | self._frozen_time_cls = freeze_time(datetime.datetime.fromtimestamp(timestamp)) 11 | self._frozen_time = None 12 | 13 | def start(self) -> freeze_time: 14 | self._frozen_time = self._frozen_time_cls.start() 15 | return self._frozen_time 16 | 17 | def advance_time(self, seconds: int) -> None: 18 | if self._frozen_time is not None: 19 | self._frozen_time.tick(datetime.timedelta(seconds=seconds+1e-2)) 20 | 21 | def stop(self) -> None: 22 | if self._frozen_time is not None: 23 | self._frozen_time_cls.stop() 24 | self._frozen_time = None 25 | 26 | def current_timestamp(self) -> Optional[float]: 27 | if self._frozen_time is not None: 28 | return self._frozen_time.time_to_freeze.timestamp() 29 | return None 30 | 31 | def current_datetime(self) -> Optional[datetime.datetime]: 32 | if self._frozen_time is not None: 33 | return self._frozen_time.time_to_freeze 34 | return None 35 | -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/unit_tests/data_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/tests/unit_tests/data_access/__init__.py -------------------------------------------------------------------------------- /backend/microservices/feedback/tests/unit_tests/data_access/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/feedback/tests/unit_tests/data_access/repository/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/microservices/location/.env: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | LOG_LEVEL=INFO 3 | 4 | ## General Settings 5 | DB_TIMEOUT=5 6 | DB_POOL_SIZE=100 7 | DB_MAX_POOL_CON=80 8 | DB_POOL_OVERFLOW=20 9 | ENABLE_DB_ECHO_LOG=False 10 | ENABLE_DB_EXPIRE_ON_COMMIT=False 11 | ENABLE_DB_FORCE_ROLLBACK=True 12 | -------------------------------------------------------------------------------- /backend/microservices/location/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 6 | ENV ENVIRONMENT=dev 7 | 8 | # Upgrade pip and install Python dependencies 9 | #RUN apt-get update 10 | #RUN apt-get install -y git 11 | RUN apt-get update && \ 12 | apt-get install -y git ca-certificates && \ 13 | apt-get clean && \ 14 | rm -rf /var/lib/apt/lists/* 15 | # Upgrade pip 16 | RUN pip install --upgrade pip 17 | 18 | # Copy requirements.txt before other files to leverage Docker cache 19 | COPY ./requirements.txt /tmp/requirements.txt 20 | 21 | 22 | RUN pip install -r /tmp/requirements.txt --no-cache-dir 23 | 24 | RUN pip install git+https://github.com/deepmancer/ftgo-utils.git 25 | 26 | # Copy the application code to the container 27 | COPY . /location 28 | 29 | # Set the working directory 30 | WORKDIR /location 31 | 32 | # Set the PYTHONPATH environment variable 33 | ENV PYTHONPATH=/location/src 34 | 35 | # RUN python -m pytest -v 36 | # Copy the Alembic configuration file 37 | COPY alembic.ini /location/alembic.ini 38 | 39 | # Run Alembic migrations and start the application 40 | CMD alembic upgrade head && python -u src/main.py 41 | -------------------------------------------------------------------------------- /backend/microservices/location/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /backend/microservices/location/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/microservices/location/migrations/versions/9fafa9afc18d_initial.py: -------------------------------------------------------------------------------- 1 | """initial 2 | 3 | Revision ID: 9fafa9afc18d 4 | Revises: 5 | Create Date: 2024-07-31 21:16:21.794914 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9fafa9afc18d' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('driver_location', 22 | sa.Column('driver_id', sa.String(), nullable=False), 23 | sa.Column('latitude', sa.Float(precision=32), nullable=False), 24 | sa.Column('longitude', sa.Float(precision=32), nullable=False), 25 | sa.Column('accuracy', sa.Float(precision=32), nullable=True), 26 | sa.Column('speed', sa.Float(precision=32), nullable=True), 27 | sa.Column('bearing', sa.Float(precision=32), nullable=True), 28 | sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False), 29 | sa.Column('id', sa.String(), nullable=False), 30 | sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 31 | sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 32 | sa.PrimaryKeyConstraint('id') 33 | ) 34 | # ### end Alembic commands ### 35 | 36 | 37 | def downgrade() -> None: 38 | # ### commands auto generated by Alembic - please adjust! ### 39 | op.drop_table('driver_location') 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /backend/microservices/location/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = 3 | test_*.py 4 | *_test.py 5 | 6 | pythonpath = src tests 7 | 8 | testpaths = 9 | tests 10 | -------------------------------------------------------------------------------- /backend/microservices/location/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.8.1 2 | asgi_lifespan 3 | asyncio 4 | asyncpg 5 | black 6 | docker 7 | dataclasses 8 | freezegun 9 | greenlet 10 | loguru 11 | mypy 12 | psycopg2-binary 13 | pytest 14 | pytest-asyncio 15 | python-decouple 16 | python-dotenv 17 | SQLAlchemy==2.0.0b3 18 | sqlalchemy 19 | pytz 20 | trio 21 | uvloop 22 | pytest 23 | pytest-asyncio 24 | pytest-cov 25 | git+https://github.com/deepmancer/ftgo-utils.git 26 | git+https://github.com/deepmancer/aredis-client.git 27 | git+https://github.com/deepmancer/asyncpg-client.git 28 | git+https://github.com/deepmancer/rabbitmq-rpc.git -------------------------------------------------------------------------------- /backend/microservices/location/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/src/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.APP.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | from application.driver import DriverService 11 | from application.tracking import TrackerService 12 | -------------------------------------------------------------------------------- /backend/microservices/location/src/application/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any, Dict 2 | from functools import wraps 3 | 4 | from config import LayerNames, BaseConfig 5 | from application import get_logger 6 | 7 | from ftgo_utils.enums import ResponseStatus 8 | from ftgo_utils.errors import ErrorCodes, BaseError, ErrorCategories 9 | 10 | logger = get_logger() 11 | 12 | def event_middleware(event_name: str, func: Callable) -> Callable: 13 | @wraps(func) 14 | async def wrapper(*args, **kwargs) -> Dict[str, Any]: 15 | try: 16 | logger.info(f"event: {event_name} is called") 17 | result = await func(*args, **kwargs) 18 | if not isinstance(result, dict) or result is None: 19 | logger.warning(f"Expected result to be a dict, got {type(result)} instead.") 20 | result = {} 21 | 22 | result['status'] = ResponseStatus.SUCCESS.value 23 | return result 24 | 25 | except BaseError as e: 26 | logger.exception(f"Error in {event_name}: {e.error_code.value}", payload=e.to_dict()) 27 | error_code = e.error_code 28 | if error_code.category != ErrorCategories.BUSINESS_LOGIC_ERROR: 29 | error_code = ErrorCodes.UNKNOWN_ERROR 30 | return { 31 | "status": ResponseStatus.FAILURE.value, 32 | "error_code": error_code.value, 33 | } 34 | 35 | except Exception as e: 36 | logger.exception(f"Error in {event_name}: {ErrorCodes.UNKNOWN_ERROR.value}", payload={"error": str(e)}) 37 | return { 38 | "status": ResponseStatus.ERROR.value, 39 | "error_code": ErrorCodes.UNKNOWN_ERROR.value, 40 | } 41 | 42 | return wrapper 43 | -------------------------------------------------------------------------------- /backend/microservices/location/src/application/tracking.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | from application import get_logger 3 | from domain.driver import Driver 4 | from ftgo_utils.errors import ErrorCodes, BaseError 5 | 6 | class TrackerService: 7 | @staticmethod 8 | async def get_nearest_drivers(location: Dict[str, Any], radius: float, max_count: int, **kwargs) -> Dict[str, Any]: 9 | latitude = location.get('latitude') 10 | longitude = location.get('longitude') 11 | if latitude is None or longitude is None: 12 | raise BaseError( 13 | code=ErrorCodes.INVALID_LOCATION_ERROR, 14 | payload={"location": location}, 15 | ) 16 | 17 | nearest_drivers = await Driver.get_nearest_drivers(latitude=latitude, longitude=longitude, radius_m=radius, max_driver_count=max_count) 18 | return {"drivers": nearest_drivers} 19 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.service import ServiceConfig 3 | from config.cache import RedisConfig 4 | from config.db import PostgresConfig 5 | from config.enums import LayerNames 6 | from config.status import DriverStatusConfig 7 | from config.hexagon import HexagonConfig 8 | from config.location import LocationConfig 9 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, TypeVar, Callable 3 | from pydantic import BaseModel, Field 4 | from decouple import config, UndefinedValueError 5 | 6 | T = TypeVar('T') 7 | 8 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 9 | try: 10 | value = config(field_name, default=default) 11 | if value is None: 12 | return default 13 | return cast_type(value) 14 | except UndefinedValueError: 15 | return default 16 | except (TypeError, ValueError) as e: 17 | if cast_type is None: 18 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 19 | else: 20 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 21 | 22 | class BaseConfig(): 23 | 24 | @classmethod 25 | def load_environment(cls): 26 | env = config("ENVIRONMENT", default='test') 27 | return env 28 | 29 | def __repr__(self): 30 | class_name = self.__class__.__name__ 31 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 32 | return f'{class_name}({attributes})' 33 | 34 | def dict(self): 35 | return self.__dict__ 36 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5672, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/cache.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class RedisConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | db: int = None, 9 | default_ttl: int = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("REDIS_HOST", "localhost") 13 | self.port = port or env_var("REDIS_PORT", 6300, int) 14 | self.db = db or env_var("REDIS_DB", 0, int) 15 | self.default_ttl = default_ttl or env_var("REDIS_DEFAULT_TTL", 600, int) 16 | self.password = password or env_var("REDIS_PASSWORD", "location_password") 17 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | APP = "app" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/hexagon.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class HexagonConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | cache_key: str = None, 7 | cache_ttl: int = None, 8 | driver_hexagon_cache_key: str = None, 9 | driver_hexagon_cache_ttl: int = None, 10 | hexagon_resolution: int = None, 11 | k_ring_radius: int = None, 12 | ): 13 | self.cache_key = cache_key or env_var("HEXAGONS_CACHE_KEY", default="hexagons_cache", cast_type=str) 14 | self.cache_ttl = cache_ttl or env_var("HEXAGONS_CACHE_TTL", default=10 * 60, cast_type=int) 15 | self.driver_hexagon_cache_key = driver_hexagon_cache_key or env_var("DRIVER_HEXAGON_CACHE_KEY", default="driver_hexagon_cache", cast_type=str) 16 | self.driver_hexagon_cache_ttl = driver_hexagon_cache_ttl or env_var("DRIVER_HEXAGON_CACHE_TTL", default=10 * 60, cast_type=int) 17 | self.hexagon_resolution = hexagon_resolution or env_var("HEXAGON_RESOLUTION", default=8, cast_type=int) 18 | self.k_ring_radius = k_ring_radius or env_var("K_RING_RADIUS", default=1, cast_type=int) 19 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/location.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class LocationConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | cache_key: str = None, 7 | cache_ttl: int = None, 8 | timestamp_maximum_delay_threshold_s: int = None, 9 | accuracy_threshold_m: int = None, 10 | maximum_speed_threshold_m: int = None, 11 | keep_last_locations_count: int = None, 12 | maximum_location_to_store_per_driver: int = None, 13 | ): 14 | self.cache_key = cache_key or env_var("LOCATIONS_CACHE_KEY", default="locations_cache", cast_type=str) 15 | self.cache_ttl = cache_ttl or env_var("LOCATIONS_CACHE_TTL", default=10 * 60, cast_type=int) 16 | self.timestamp_maximum_delay_threshold_s = timestamp_maximum_delay_threshold_s or env_var("TIMESTAMP_MAXIMUM_DELAY_THRESHOLD_S", default=5 * 60, cast_type=int) 17 | self.accuracy_threshold_m = accuracy_threshold_m or env_var("ACCURACY_THRESHOLD_M", default=15, cast_type=int) 18 | self.maximum_speed_threshold_m = maximum_speed_threshold_m or env_var("MAXIMUM_SPEED_THRESHOLD_M", default=150, cast_type=int) 19 | self.keep_last_locations_count = keep_last_locations_count or env_var("KEEP_LAST_LOCATIONS_COUNT", default=5, cast_type=int) 20 | self.maximum_location_to_store_per_driver = maximum_location_to_store_per_driver or env_var("MAXIMUM_LOCATION_TO_STORE_PER_DRIVER", default=20, cast_type=int) 21 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import BaseConfig, env_var 3 | 4 | class ServiceConfig(BaseConfig): 5 | def __init__( 6 | self, 7 | environment: str = None, 8 | log_level_name: str = None, 9 | ): 10 | self.environment = environment or env_var('ENVIRONMENT', default='test') 11 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 12 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 13 | -------------------------------------------------------------------------------- /backend/microservices/location/src/config/status.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class DriverStatusConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | cache_key: str = None, 7 | persistent_ttl: int = None, 8 | ): 9 | self.cache_key = cache_key or env_var("DRIVER_STATUS_CACHE_KEY", default="driver_status", cast_type=str) 10 | self.cache_ttl = persistent_ttl or env_var("DRIVER_STATUS_CACHE_TTL", default=600, cast_type=int) 11 | -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from rabbitmq_rpc import RPCClient 5 | 6 | from data_access.repository.cache_repository import CacheRepository 7 | from data_access.repository.db_repository import DatabaseRepository 8 | from data_access.broker import RPCBroker 9 | 10 | from config import BaseConfig 11 | from data_access import get_logger 12 | 13 | async def setup() -> None: 14 | logger = get_logger() 15 | await CacheRepository.initialize() 16 | logger.info("Connected to Redis") 17 | await DatabaseRepository.initialize() 18 | logger.info("Connected to PostgreSQL") 19 | await RPCBroker.initialize(asyncio.get_event_loop()) 20 | logger.info("Connected to RabbitMQ") 21 | 22 | 23 | async def teardown() -> None: 24 | logger = get_logger() 25 | await CacheRepository.terminate() 26 | logger.info("Disconnected from Redis") 27 | await DatabaseRepository.terminate() 28 | logger.info("Disconnected from PostgreSQL") 29 | await RPCBroker.terminate() 30 | logger.info("Disconnected from RabbitMQ") -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/models/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.models.base import Base 2 | from data_access.models.driver_location import DriverLocation 3 | -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/models/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Type, Any 3 | 4 | import sqlalchemy 5 | from sqlalchemy import DateTime, func 6 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 7 | 8 | from ftgo_utils.uuid_gen import uuid4 9 | 10 | from dto import BaseDTO 11 | 12 | class Base(DeclarativeBase): 13 | __abstract__ = True 14 | metadata = sqlalchemy.MetaData() 15 | 16 | id: Mapped[str] = mapped_column( 17 | sqlalchemy.String, primary_key=True, default=uuid4 18 | ) 19 | created_at: Mapped[datetime] = mapped_column( 20 | DateTime(timezone=True), server_default=func.now() 21 | ) 22 | updated_at: Mapped[datetime] = mapped_column( 23 | DateTime(timezone=True), server_default=func.now(), onupdate=func.now() 24 | ) 25 | 26 | def to_dict(self) -> Dict[str, Any]: 27 | return {column.name: getattr(self, column.name) for column in self.__table__.columns} 28 | 29 | def __repr__(self) -> str: 30 | return f"<{self.__class__.__name__}({self.to_dict()})>" 31 | 32 | @classmethod 33 | def from_dto(cls, dto: BaseDTO) -> 'Base': 34 | raise NotImplementedError 35 | 36 | def to_dto(self) -> BaseDTO: 37 | raise NotImplementedError 38 | -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.repository.cache_repository import CacheRepository 2 | from data_access.repository.db_repository import DatabaseRepository 3 | -------------------------------------------------------------------------------- /backend/microservices/location/src/data_access/repository/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class BaseRepository: 5 | _data_access = None 6 | 7 | @classmethod 8 | async def insert(cls, *args, **kwargs) -> Any: 9 | raise NotImplementedError 10 | 11 | @classmethod 12 | async def delete(cls, *args, **kwargs) -> Any: 13 | raise NotImplementedError 14 | 15 | @classmethod 16 | async def update(cls, *args, **kwargs) -> Any: 17 | raise NotImplementedError 18 | 19 | @classmethod 20 | async def fetch(cls, *args, **kwargs) -> Any: 21 | raise NotImplementedError 22 | 23 | @classmethod 24 | async def initialize(cls) -> None: 25 | raise NotImplementedError 26 | 27 | @classmethod 28 | async def terminate(cls) -> None: 29 | if cls._data_access: 30 | await cls._data_access.disconnect() 31 | cls._data_access = None 32 | -------------------------------------------------------------------------------- /backend/microservices/location/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DOMAIN.value 6 | 7 | def get_logger(layer: str =layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/location/src/dto/__init__.py: -------------------------------------------------------------------------------- 1 | from dto.base import BaseDTO 2 | from dto.location import DriverLocationDTO 3 | -------------------------------------------------------------------------------- /backend/microservices/location/src/dto/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from dataclasses import dataclass, asdict 3 | from typing import Optional 4 | 5 | @dataclass 6 | class BaseDTO: 7 | created_at: Optional[datetime] = None 8 | 9 | def to_dict(self): 10 | return asdict(self) 11 | -------------------------------------------------------------------------------- /backend/microservices/location/src/dto/location.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | from dto.base import BaseDTO 6 | 7 | @dataclass 8 | class DriverLocationDTO(BaseDTO): 9 | location_id: Optional[str] = None 10 | driver_id: Optional[str] = None 11 | latitude: Optional[float] = None 12 | longitude: Optional[float] = None 13 | accuracy: Optional[float] = None 14 | speed: Optional[float] = None 15 | bearing: Optional[float] = None 16 | timestamp: Optional[datetime] = None 17 | -------------------------------------------------------------------------------- /backend/microservices/location/src/events.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from ftgo_utils.logger import get_logger 4 | from ftgo_utils.errors import ErrorCodes 5 | 6 | from application import DriverService, TrackerService 7 | from application.middleware import event_middleware 8 | from config import LayerNames 9 | from data_access.broker import RPCBroker 10 | from utils import handle_exception 11 | 12 | async def register_events(): 13 | rpc_broker = RPCBroker.get_instance() 14 | rpc_client = rpc_broker.get_client() 15 | events_handlers = { 16 | 'driver.location.submit': DriverService.submit_location, 17 | 'driver.status.online': DriverService.change_status_online, 18 | 'driver.status.offline': DriverService.change_status_offline, 19 | 'driver.availability.available': DriverService.set_driver_available, 20 | 'driver.availability.occupied': DriverService.set_driver_occupied, 21 | 'driver.location.get': DriverService.get_last_location, 22 | 'driver.status.get': DriverService.get_driver_status, 23 | 'location.drivers.get_nearest': TrackerService.get_nearest_drivers, 24 | } 25 | 26 | for event, _handler in events_handlers.items(): 27 | try: 28 | handler = event_middleware(event, _handler) 29 | await rpc_client.register_event(event=event, handler=handler) 30 | rpc_client.logger.info(f"Registered event '{event}' with handler '{handler.__name__}'") 31 | except Exception as e: 32 | payload = {"event_name": event} 33 | rpc_client.logger.exception(ErrorCodes.EVENT_REGISTERATION_ERROR.value, payload=payload) 34 | await handle_exception(e=e, error_code=ErrorCodes.EVENT_REGISTERATION_ERROR, payload=payload) 35 | -------------------------------------------------------------------------------- /backend/microservices/location/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvloop 3 | from dotenv import load_dotenv 4 | 5 | from ftgo_utils.logger import init_logging 6 | 7 | from config import ServiceConfig 8 | from data_access.events.lifecycle import setup, teardown 9 | from events import register_events 10 | 11 | load_dotenv() 12 | 13 | async def setup_env(): 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def startup_event(): 18 | await setup_env() 19 | await setup() 20 | await asyncio.sleep(1) 21 | await register_events() 22 | await asyncio.Future() 23 | 24 | async def shutdown_event(): 25 | await teardown() 26 | 27 | if __name__ == '__main__': 28 | uvloop.install() 29 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 30 | loop = asyncio.get_event_loop() 31 | 32 | try: 33 | loop.run_until_complete(startup_event()) 34 | finally: 35 | loop.run_until_complete(shutdown_event()) 36 | loop.close() 37 | -------------------------------------------------------------------------------- /backend/microservices/location/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/microservices/location/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/microservices/location/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import pytest_asyncio 4 | 5 | from test_doubles.redis import FakeAsyncRedis 6 | from test_doubles.time import TimeProvider 7 | from data_access.repository.cache_repository import CacheRepository 8 | 9 | MOCKED_TIMESTAMP = 1704067200 # 2024, January 1 10 | 11 | @pytest.fixture(scope='function') 12 | def time_machine(): 13 | provider = TimeProvider(timestamp=MOCKED_TIMESTAMP) 14 | provider.start() 15 | yield provider 16 | provider.stop() 17 | 18 | @pytest_asyncio.fixture(scope='function') 19 | async def fake_redis(time_machine: TimeProvider): 20 | redis = await FakeAsyncRedis.create( 21 | host="localhost", 22 | port=6379, 23 | db=0, 24 | time_provider=time_machine.current_timestamp 25 | ) 26 | return redis 27 | 28 | @pytest_asyncio.fixture(scope='function') 29 | async def cache_repository(fake_redis): 30 | CacheRepository._data_access = fake_redis 31 | yield CacheRepository 32 | 33 | @pytest_asyncio.fixture 34 | async def setup_and_teardown_cache(cache_repository): 35 | """Setup and teardown for each test using CacheRepository.""" 36 | async with cache_repository._data_access.get_or_create_session() as session: 37 | await session.flushdb() 38 | yield 39 | async with cache_repository._data_access.get_or_create_session() as session: 40 | await session.flushdb() 41 | -------------------------------------------------------------------------------- /backend/microservices/location/tests/test_doubles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/tests/test_doubles/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/tests/test_doubles/time.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from typing import Optional 4 | 5 | from freezegun import freeze_time 6 | 7 | 8 | class TimeProvider: 9 | def __init__(self, timestamp: float) -> None: 10 | self._frozen_time_cls = freeze_time(datetime.datetime.fromtimestamp(timestamp)) 11 | self._frozen_time = None 12 | 13 | def start(self) -> freeze_time: 14 | self._frozen_time = self._frozen_time_cls.start() 15 | return self._frozen_time 16 | 17 | def advance_time(self, seconds: int) -> None: 18 | if self._frozen_time is not None: 19 | self._frozen_time.tick(datetime.timedelta(seconds=seconds+1e-2)) 20 | 21 | def stop(self) -> None: 22 | if self._frozen_time is not None: 23 | self._frozen_time_cls.stop() 24 | self._frozen_time = None 25 | 26 | def current_timestamp(self) -> Optional[float]: 27 | if self._frozen_time is not None: 28 | return self._frozen_time.time_to_freeze.timestamp() 29 | return None 30 | 31 | def current_datetime(self) -> Optional[datetime.datetime]: 32 | if self._frozen_time is not None: 33 | return self._frozen_time.time_to_freeze 34 | return None 35 | -------------------------------------------------------------------------------- /backend/microservices/location/tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/tests/unit_tests/data_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/tests/unit_tests/data_access/__init__.py -------------------------------------------------------------------------------- /backend/microservices/location/tests/unit_tests/data_access/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/location/tests/unit_tests/data_access/repository/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/microservices/order/.env: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | LOG_LEVEL=INFO 3 | -------------------------------------------------------------------------------- /backend/microservices/order/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 6 | ENV ENVIRONMENT=dev 7 | 8 | # Upgrade pip and install Python dependencies 9 | #RUN apt-get update 10 | #RUN apt-get install -y git 11 | RUN apt-get update && \ 12 | apt-get install -y git ca-certificates && \ 13 | apt-get clean && \ 14 | rm -rf /var/lib/apt/lists/* 15 | # Upgrade pip 16 | RUN pip install --upgrade pip 17 | 18 | # Copy requirements.txt before other files to leverage Docker cache 19 | COPY ./requirements.txt /tmp/requirements.txt 20 | 21 | 22 | RUN pip install -r /tmp/requirements.txt --no-cache-dir --force-reinstall 23 | RUN pip install --upgrade --no-deps git+https://github.com/deepmancer/aredis-client.git 24 | # Copy the application code to the container 25 | COPY . /order 26 | 27 | # Set the working directory 28 | WORKDIR /order 29 | 30 | # Set the PYTHONPATH environment variable 31 | ENV PYTHONPATH=/order/src 32 | 33 | # RUN python -m pytest -v 34 | # Copy the Alembic configuration file 35 | # COPY alembic.ini /order/alembic.ini 36 | 37 | # Run Alembic migrations and start the application 38 | CMD python -u src/main.py 39 | -------------------------------------------------------------------------------- /backend/microservices/order/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = 3 | test_*.py 4 | *_test.py 5 | 6 | pythonpath = src tests 7 | 8 | testpaths = 9 | tests 10 | -------------------------------------------------------------------------------- /backend/microservices/order/requirements.txt: -------------------------------------------------------------------------------- 1 | asgi_lifespan 2 | asyncio 3 | beanie 4 | black 5 | dataclasses 6 | docker 7 | freezegun 8 | greenlet 9 | loguru 10 | motor 11 | pymongo 12 | pydantic>2.0 13 | python-decouple 14 | python-dotenv 15 | pytest 16 | pytest-asyncio 17 | pytest-cov 18 | pytz 19 | trio 20 | uvloop 21 | 22 | git+https://github.com/deepmancer/aredis-client.git 23 | git+https://github.com/deepmancer/ftgo-utils.git 24 | git+https://github.com/deepmancer/mongo-motors.git 25 | git+https://github.com/deepmancer/rabbitmq-rpc.git -------------------------------------------------------------------------------- /backend/microservices/order/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/src/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.APP.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | -------------------------------------------------------------------------------- /backend/microservices/order/src/application/delivery.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from ftgo_utils.enums import DeliveryStatus 3 | from domain.delivery import DeliveryHandler 4 | 5 | class DeliveryService: 6 | @staticmethod 7 | async def schedule_delivery( 8 | order_id: str, 9 | driver_id: str, 10 | source_address_id: str, 11 | destination_address_id: str, 12 | **kwargs, 13 | ) -> Dict[str, Any]: 14 | return await DeliveryHandler.schedule_delivery( 15 | order_id=order_id, 16 | driver_id=driver_id, 17 | source_address_id=source_address_id, 18 | destination_address_id=destination_address_id, 19 | **kwargs 20 | ) 21 | 22 | @staticmethod 23 | async def update_delivery_status(delivery_id: str, status: DeliveryStatus, **kwargs) -> Dict[str, Any]: 24 | return await DeliveryHandler.update_delivery_status(delivery_id, status, **kwargs) 25 | 26 | @staticmethod 27 | async def get_delivery_details(delivery_id: str, **kwargs) -> Dict[str, Any]: 28 | return await DeliveryHandler.get_delivery_details(delivery_id, **kwargs) 29 | 30 | @staticmethod 31 | async def assign_driver_to_delivery(driver_id: str, delivery_id: str, **kwargs) -> Dict[str, Any]: 32 | return await DeliveryHandler.assign_driver_to_order(driver_id, delivery_id, **kwargs) 33 | -------------------------------------------------------------------------------- /backend/microservices/order/src/application/order.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from domain.order import OrderHandler 3 | 4 | class OrderService: 5 | @staticmethod 6 | async def create_order( 7 | customer_id: str, 8 | restaurant_id: str, 9 | order_items_data: List[Dict[str, Any]], 10 | special_instructions: Optional[str] = None, 11 | **kwargs, 12 | ) -> Dict[str, Any]: 13 | return await OrderHandler.create_order( 14 | customer_id=customer_id, 15 | restaurant_id=restaurant_id, 16 | order_items_data=order_items_data, 17 | special_instructions=special_instructions, 18 | **kwargs 19 | ) 20 | 21 | @staticmethod 22 | async def update_order(order_id: str, updated_items_data: List[Dict[str, Any]], **kwargs) -> Dict[str, Any]: 23 | return await OrderHandler.update_order(order_id, updated_items_data, **kwargs) 24 | 25 | @staticmethod 26 | async def get_order_details(order_id: str, **kwargs) -> Dict[str, Any]: 27 | return await OrderHandler.get_order_details(order_id, **kwargs) 28 | -------------------------------------------------------------------------------- /backend/microservices/order/src/application/order_status.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Any 2 | from ftgo_utils.enums import OrderStatus as OrderStatusEnum 3 | from domain.order_status import OrderStatusHandler 4 | 5 | class OrderStatusService: 6 | @staticmethod 7 | async def change_order_status(order_id: str, new_status: OrderStatusEnum, changed_by: Optional[str] = None, comments: Optional[str] = None, **kwargs) -> Dict[str, Any]: 8 | return await OrderStatusHandler.change_order_status( 9 | order_id=order_id, 10 | new_status=new_status, 11 | changed_by=changed_by, 12 | comments=comments, 13 | **kwargs 14 | ) 15 | 16 | @staticmethod 17 | async def cancel_order(order_id: str, **kwargs) -> Dict[str, Any]: 18 | return await OrderStatusHandler.cancel_order(order_id, **kwargs) 19 | 20 | @staticmethod 21 | async def mark_order_ready_for_pickup(order_id: str, **kwargs) -> Dict[str, Any]: 22 | return await OrderStatusHandler.mark_order_ready_for_pickup(order_id, **kwargs) 23 | -------------------------------------------------------------------------------- /backend/microservices/order/src/application/restaurant.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Any 2 | from ftgo_utils.enums import OrderStatus as OrderStatusEnum 3 | from domain.restaurant import RestaurantHandler 4 | 5 | class RestaurantService: 6 | @staticmethod 7 | async def confirm_order(order_id: str, **kwargs) -> Dict[str, Any]: 8 | return await RestaurantHandler.confirm_order(order_id, **kwargs) 9 | 10 | @staticmethod 11 | async def reject_order(order_id: str, reason: Optional[str] = None, **kwargs) -> Dict[str, Any]: 12 | return await RestaurantHandler.reject_order(order_id, reason, **kwargs) 13 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.service import ServiceConfig 3 | from config.db import MongoConfig 4 | from config.enums import LayerNames 5 | from config.cache import RedisConfig 6 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, TypeVar, Callable 3 | from decouple import config, UndefinedValueError 4 | 5 | T = TypeVar('T') 6 | 7 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 8 | try: 9 | value = config(field_name, default=default) 10 | if value is None: 11 | return default 12 | return cast_type(value) 13 | except UndefinedValueError: 14 | return default 15 | except (TypeError, ValueError) as e: 16 | if cast_type is None: 17 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 18 | else: 19 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 20 | 21 | class BaseConfig(): 22 | @classmethod 23 | def load_environment(cls): 24 | env = config("ENVIRONMENT", default='test') 25 | return env 26 | 27 | def __repr__(self): 28 | class_name = self.__class__.__name__ 29 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 30 | return f'{class_name}({attributes})' 31 | 32 | def dict(self): 33 | return self.__dict__ 34 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5672, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/cache.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class RedisConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | db: int = None, 9 | default_ttl: int = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("REDIS_HOST", "localhost") 13 | self.port = port or env_var("REDIS_PORT", 6301, int) 14 | self.db = db or env_var("REDIS_DB", 0, int) 15 | self.default_ttl = default_ttl or env_var("REDIS_DEFAULT_TTL", 60, int) 16 | self.password = password or env_var("REDIS_PASSWORD", "order_password") 17 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/db.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class MongoConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | database: str = None, 9 | username: str = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("MONGO_HOST", default="localhost") 13 | self.port = port or env_var("MONGO_PORT", default=7017, cast_type=int) 14 | self.database = database or env_var("MONGO_DATABASE", default="order_database") 15 | self.username = username or env_var("MONGO_USERNAME", default="order_user") 16 | self.password = password or env_var("MONGO_PASSWORD", default="order_password") 17 | 18 | @property 19 | def url(self): 20 | return f"mongodb://{self.username}:{self.password}@{self.host}:{self.port}" 21 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | APP = "app" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | -------------------------------------------------------------------------------- /backend/microservices/order/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import BaseConfig, env_var 3 | 4 | class ServiceConfig(BaseConfig): 5 | def __init__( 6 | self, 7 | environment: str = None, 8 | log_level_name: str = None, 9 | ): 10 | self.environment = environment or env_var('ENVIRONMENT', default='test') 11 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 12 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 13 | -------------------------------------------------------------------------------- /backend/microservices/order/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/order/src/data_access/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class BaseRepository: 5 | _data_access = None 6 | 7 | @classmethod 8 | async def initialize(cls) -> None: 9 | raise NotImplementedError 10 | 11 | @classmethod 12 | async def terminate(cls) -> None: 13 | if cls._data_access: 14 | await cls._data_access.disconnect() 15 | cls._data_access = None 16 | -------------------------------------------------------------------------------- /backend/microservices/order/src/data_access/db_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from beanie import init_beanie 3 | from mongo_motors import AsyncMongo 4 | 5 | from ftgo_utils.errors import ErrorCodes 6 | 7 | from config import MongoConfig 8 | from data_access import get_logger 9 | from data_access.base import BaseRepository 10 | from models import DeliveryDetail, Order, OrderItem, OrderStatus 11 | from utils import handle_exception 12 | 13 | class DatabaseRepository(BaseRepository): 14 | _data_access: Optional[AsyncMongo] = None 15 | 16 | @classmethod 17 | async def initialize(cls) -> None: 18 | db_config = MongoConfig() 19 | try: 20 | mongo_data_access = await AsyncMongo.create( 21 | host=db_config.host, 22 | port=db_config.port, 23 | database=db_config.database, 24 | username=db_config.username, 25 | password=db_config.password, 26 | ) 27 | await init_beanie( 28 | database=mongo_data_access.get_database(), 29 | document_models=[Order, OrderStatus, OrderItem, DeliveryDetail], 30 | ) 31 | cls._data_access = mongo_data_access 32 | 33 | except Exception as e: 34 | payload = db_config.dict() 35 | get_logger().error(ErrorCodes.DB_CONNECTION_ERROR.value, payload=payload) 36 | await handle_exception(e=e, error_code=ErrorCodes.DB_CONNECTION_ERROR, payload=payload) 37 | 38 | @classmethod 39 | async def terminate(cls) -> None: 40 | if cls._data_access: 41 | await cls._data_access.disconnect() 42 | cls._data_access = None 43 | -------------------------------------------------------------------------------- /backend/microservices/order/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from config import BaseConfig 5 | from data_access import get_logger 6 | from data_access.broker import RPCBroker 7 | from data_access.cache_repository import CacheRepository 8 | from data_access.db_repository import DatabaseRepository 9 | 10 | 11 | async def setup() -> None: 12 | logger = get_logger() 13 | await CacheRepository.initialize() 14 | get_logger().info("Connected to Redis") 15 | await DatabaseRepository.initialize() 16 | logger.info("Connected to MongoDB") 17 | await RPCBroker.initialize(asyncio.get_event_loop()) 18 | logger.info("Connected to RabbitMQ") 19 | 20 | 21 | async def teardown() -> None: 22 | logger = get_logger() 23 | await CacheRepository.terminate() 24 | get_logger().info("Disconnected from Redis") 25 | await DatabaseRepository.terminate() 26 | logger.info("Disconnected from MongoDB") 27 | await RPCBroker.terminate() 28 | logger.info("Disconnected from RabbitMQ") 29 | -------------------------------------------------------------------------------- /backend/microservices/order/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DOMAIN.value 6 | 7 | def get_logger(layer: str =layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/order/src/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from domain.entities.base import BaseEntity 2 | from domain.entities.delivery import Delivery 3 | from domain.entities.order import Order 4 | from domain.entities.order_item import OrderItem 5 | from domain.entities.order_status import OrderStatus 6 | -------------------------------------------------------------------------------- /backend/microservices/order/src/domain/entities/base.py: -------------------------------------------------------------------------------- 1 | from pydoc import doc 2 | from beanie import Document 3 | from config import RedisConfig 4 | from ftgo_utils.errors import ErrorCodes 5 | from typing import Any, Dict, Optional 6 | from data_access.cache_repository import CacheRepository 7 | 8 | 9 | class BaseEntity: 10 | document_cls = None 11 | 12 | def __init__(self, document: Document): 13 | self.document: Document = document 14 | 15 | @classmethod 16 | def create(cls, **kwargs) -> "BaseEntity": 17 | raise NotImplementedError("The 'create' method must be implemented in the subclass.") 18 | 19 | @classmethod 20 | def build_query(cls, **kwargs) -> Dict[str, Any]: 21 | query = {} 22 | for key, value in kwargs.items(): 23 | if value is not None: 24 | query[key] = value 25 | return query 26 | 27 | @classmethod 28 | async def fetch_document(cls, **kwargs) -> Optional[Document]: 29 | query = cls.build_query(**kwargs) 30 | document = await cls.document_cls.find(**query).first_or_none() 31 | return document 32 | 33 | @classmethod 34 | async def load(cls, **kwargs) -> Optional["BaseEntity"]: 35 | raise NotImplementedError("The 'save' method must be implemented in the subclass.") 36 | 37 | async def save(self): 38 | raise NotImplementedError("The 'save' method must be implemented in the subclass.") 39 | 40 | async def delete(self): 41 | raise NotImplementedError("The 'delete' method must be implemented in the subclass.") 42 | 43 | async def update(self, **kwargs): 44 | raise NotImplementedError("The 'update' method must be implemented in the subclass.") 45 | -------------------------------------------------------------------------------- /backend/microservices/order/src/domain/restaurant.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Any 2 | from ftgo_utils.enums import OrderStatus as OrderStatusEnum 3 | from domain.entities import Order 4 | 5 | class RestaurantHandler: 6 | @staticmethod 7 | async def confirm_order(order_id: str, **kwargs) -> Dict[str, Any]: 8 | order = await RestaurantHandler.load_entity(Order, order_id) 9 | await order.change_status(OrderStatusEnum.CONFIRMED) 10 | await RestaurantHandler.save_entity(order) 11 | return order.document.dict() 12 | 13 | @staticmethod 14 | async def reject_order(order_id: str, reason: Optional[str] = None, **kwargs) -> Dict[str, Any]: 15 | order = await RestaurantHandler.load_entity(Order, order_id) 16 | await order.change_status(OrderStatusEnum.REJECTED, comments=reason) 17 | await RestaurantHandler.save_entity(order) 18 | return order.document.dict() 19 | -------------------------------------------------------------------------------- /backend/microservices/order/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvloop 3 | from dotenv import load_dotenv 4 | 5 | from ftgo_utils.logger import init_logging 6 | 7 | from config import ServiceConfig 8 | from data_access.events.lifecycle import setup, teardown 9 | from events import register_events 10 | 11 | load_dotenv() 12 | 13 | async def setup_env(): 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def startup_event(): 18 | await setup_env() 19 | await setup() 20 | await asyncio.sleep(1) 21 | await register_events() 22 | await asyncio.Future() 23 | 24 | async def shutdown_event(): 25 | await teardown() 26 | 27 | if __name__ == '__main__': 28 | uvloop.install() 29 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 30 | loop = asyncio.get_event_loop() 31 | 32 | try: 33 | loop.run_until_complete(startup_event()) 34 | finally: 35 | loop.run_until_complete(shutdown_event()) 36 | loop.close() 37 | -------------------------------------------------------------------------------- /backend/microservices/order/src/models/__init__.py: -------------------------------------------------------------------------------- 1 | from models.delivery_detail import DeliveryDetail 2 | from models.order_item import OrderItem 3 | from models.order_status import OrderStatus 4 | from models.order import Order 5 | -------------------------------------------------------------------------------- /backend/microservices/order/src/models/delivery_detail.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from beanie import Document 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | 9 | 10 | 11 | class DeliveryDetail(Document): 12 | order_id: str 13 | driver_id: str 14 | delivery_status: str = Field(..., max_length=50) 15 | source_address_id: str 16 | destination_address_id: str 17 | 18 | created_at: datetime = Field(default_factory=datetime.utcnow) 19 | updated_at: datetime = Field(default_factory=datetime.utcnow) 20 | 21 | class Settings: 22 | name = "delivery_details" 23 | indexes = [ 24 | IndexModel([("delivery_status", pymongo.ASCENDING)], name="delivery_detail_delivery_status_index"), 25 | IndexModel([("order_id", pymongo.ASCENDING)], name="delivery_detail_order_id_index"), 26 | IndexModel([("driver_id", pymongo.ASCENDING)], name="delivery_detail_driver_id_index"), 27 | ] 28 | use_state_management = True 29 | validate_on_save = True 30 | 31 | @classmethod 32 | async def before_insert(cls, instance): 33 | instance.created_at = datetime.utcnow() 34 | instance.updated_at = datetime.utcnow() 35 | 36 | @classmethod 37 | async def before_replace(cls, instance): 38 | instance.updated_at = datetime.utcnow() 39 | -------------------------------------------------------------------------------- /backend/microservices/order/src/models/order.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional, List 5 | from beanie import Document, Link 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | from models.order_item import OrderItem 9 | from models.order_status import OrderStatus 10 | 11 | class Order(Document): 12 | customer_id: str 13 | restaurant_id: str 14 | total_amount: float = Field(..., gt=0) 15 | status: OrderStatus 16 | order_items: List[Link[OrderItem]] = [] 17 | status_history: Optional[List[Link[OrderStatus]]] = [] 18 | payment_id: Optional[str] = None 19 | special_instructions: Optional[str] = None 20 | created_at: datetime = Field(default_factory=datetime.utcnow) 21 | updated_at: datetime = Field(default_factory=datetime.utcnow) 22 | 23 | class Settings: 24 | name = "orders" 25 | indexes = [ 26 | IndexModel([("customer_id", pymongo.ASCENDING)], name="order_customer_id_index"), 27 | IndexModel([("restaurant_id", pymongo.ASCENDING)], name="order_restaurant_id_index"), 28 | IndexModel([("created_at", pymongo.DESCENDING)], name="order_created_at_index"), 29 | ] 30 | use_state_management = True 31 | validate_on_save = True 32 | 33 | @classmethod 34 | async def before_insert(cls, instance): 35 | instance.created_at = datetime.utcnow() 36 | instance.updated_at = datetime.utcnow() 37 | 38 | @classmethod 39 | async def before_replace(cls, instance): 40 | instance.updated_at = datetime.utcnow() 41 | -------------------------------------------------------------------------------- /backend/microservices/order/src/models/order_item.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from beanie import Document, Link 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | 9 | class OrderItem(Document): 10 | order_id: str 11 | menu_item_id: str 12 | quantity: int = Field(..., gt=0) 13 | item_price: float = Field(..., gt=0) 14 | subtotal: float = Field(..., gt=0) 15 | special_instructions: Optional[str] = None 16 | 17 | created_at: datetime = Field(default_factory=datetime.utcnow) 18 | updated_at: datetime = Field(default_factory=datetime.utcnow) 19 | 20 | class Settings: 21 | name = "order_items" 22 | indexes = [ 23 | IndexModel([("order_id", pymongo.ASCENDING)], name="order_item_order_id_index"), 24 | IndexModel([("menu_item_id", pymongo.ASCENDING)], name="order_item_menu_item_id_index"), 25 | ] 26 | use_state_management = True 27 | validate_on_save = True 28 | 29 | @classmethod 30 | async def before_insert(cls, instance): 31 | instance.created_at = datetime.utcnow() 32 | instance.updated_at = datetime.utcnow() 33 | 34 | @classmethod 35 | async def before_replace(cls, instance): 36 | instance.updated_at = datetime.utcnow() 37 | -------------------------------------------------------------------------------- /backend/microservices/order/src/models/order_status.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from beanie import Document, Link 6 | from pydantic import Field 7 | from pymongo import IndexModel 8 | 9 | class OrderStatus(Document): 10 | order_id: str 11 | status: str = Field(..., max_length=50) 12 | changed_by: Optional[str] = None 13 | comments: Optional[str] = None 14 | 15 | created_at: datetime = Field(default_factory=datetime.utcnow) 16 | updated_at: datetime = Field(default_factory=datetime.utcnow) 17 | 18 | class Settings: 19 | name = "order_status" 20 | indexes = [ 21 | IndexModel([("order_id", pymongo.ASCENDING)], name="order_status_order_id_index"), 22 | IndexModel([("status", pymongo.ASCENDING)], name="order_status_status_index"), 23 | ] 24 | use_state_management = True 25 | validate_on_save = True 26 | 27 | @classmethod 28 | async def before_insert(cls, instance): 29 | instance.created_at = datetime.utcnow() 30 | instance.updated_at = datetime.utcnow() 31 | 32 | @classmethod 33 | async def before_replace(cls, instance): 34 | instance.updated_at = datetime.utcnow() 35 | -------------------------------------------------------------------------------- /backend/microservices/order/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/microservices/order/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/microservices/order/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import pytest_asyncio 4 | 5 | from test_doubles.time import TimeProvider 6 | 7 | MOCKED_TIMESTAMP = 1704067200 # 2024, January 1 8 | 9 | @pytest.fixture(scope='function') 10 | def time_machine(): 11 | provider = TimeProvider(timestamp=MOCKED_TIMESTAMP) 12 | provider.start() 13 | yield provider 14 | provider.stop() 15 | 16 | -------------------------------------------------------------------------------- /backend/microservices/order/tests/test_doubles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/tests/test_doubles/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/tests/test_doubles/time.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from typing import Optional 4 | 5 | from freezegun import freeze_time 6 | 7 | 8 | class TimeProvider: 9 | def __init__(self, timestamp: float) -> None: 10 | self._frozen_time_cls = freeze_time(datetime.datetime.fromtimestamp(timestamp)) 11 | self._frozen_time = None 12 | 13 | def start(self) -> freeze_time: 14 | self._frozen_time = self._frozen_time_cls.start() 15 | return self._frozen_time 16 | 17 | def advance_time(self, seconds: int) -> None: 18 | if self._frozen_time is not None: 19 | self._frozen_time.tick(datetime.timedelta(seconds=seconds+1e-2)) 20 | 21 | def stop(self) -> None: 22 | if self._frozen_time is not None: 23 | self._frozen_time_cls.stop() 24 | self._frozen_time = None 25 | 26 | def current_timestamp(self) -> Optional[float]: 27 | if self._frozen_time is not None: 28 | return self._frozen_time.time_to_freeze.timestamp() 29 | return None 30 | 31 | def current_datetime(self) -> Optional[datetime.datetime]: 32 | if self._frozen_time is not None: 33 | return self._frozen_time.time_to_freeze 34 | return None 35 | -------------------------------------------------------------------------------- /backend/microservices/order/tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/tests/unit_tests/data_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/tests/unit_tests/data_access/__init__.py -------------------------------------------------------------------------------- /backend/microservices/order/tests/unit_tests/data_access/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/order/tests/unit_tests/data_access/repository/__init__.py -------------------------------------------------------------------------------- /backend/microservices/restaurant/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/microservices/restaurant/.env: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | LOG_LEVEL=INFO 3 | 4 | ## General Settings 5 | DB_TIMEOUT=5 6 | DB_POOL_SIZE=100 7 | DB_MAX_POOL_CON=80 8 | DB_POOL_OVERFLOW=20 9 | ENABLE_DB_ECHO_LOG=False 10 | ENABLE_DB_EXPIRE_ON_COMMIT=False 11 | ENABLE_DB_FORCE_ROLLBACK=True 12 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | .DS_Store 4 | .idea/ 5 | .vscode/ 6 | 7 | # Exclude testing cache 8 | .pytest_cache/ -------------------------------------------------------------------------------- /backend/microservices/restaurant/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 6 | ENV ENVIRONMENT=dev 7 | 8 | # Upgrade pip and install Python dependencies 9 | #RUN apt-get update 10 | #RUN apt-get install -y git 11 | RUN apt-get update && apt-get install -y git ca-certificates 12 | 13 | # Upgrade pip 14 | RUN pip install --upgrade pip 15 | 16 | # Copy requirements.txt before other files to leverage Docker cache 17 | COPY ./requirements.txt /tmp/requirements.txt 18 | 19 | RUN pip install -r /tmp/requirements.txt --force-reinstall --upgrade 20 | 21 | # Copy the application code to the container 22 | COPY . /restaurant 23 | 24 | # Set the working directory 25 | WORKDIR /restaurant 26 | 27 | # Set the PYTHONPATH environment variable 28 | ENV PYTHONPATH=/restaurant/src 29 | 30 | # Copy the Alembic configuration file 31 | COPY alembic.ini /restaurant/alembic.ini 32 | 33 | # Run Alembic migrations and start the application 34 | CMD alembic upgrade head && python src/main.py 35 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /backend/microservices/restaurant/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.8.1 2 | asgi_lifespan 3 | asyncio 4 | asyncpg 5 | black 6 | docker 7 | greenlet 8 | loguru 9 | mypy 10 | psycopg2-binary 11 | pydantic 12 | pydantic-settings 13 | pytest 14 | pytest-asyncio 15 | python-decouple 16 | python-dotenv 17 | SQLAlchemy==2.0.0b3 18 | sqlalchemy 19 | pytz 20 | trio 21 | uvloop 22 | git+https://github.com/deepmancer/ftgo-utils.git 23 | git+https://github.com/deepmancer/aredis-client.git 24 | git+https://github.com/deepmancer/asyncpg-client.git 25 | git+https://github.com/deepmancer/rabbitmq-rpc.git -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/restaurant/src/__init__.py -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.APP.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | from application.menu import MenuService 11 | from application.supplier import RestaurantService 12 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/application/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any, Dict 2 | from functools import wraps 3 | 4 | from config import LayerNames, BaseConfig 5 | from application import get_logger 6 | 7 | from ftgo_utils.enums import ResponseStatus 8 | from ftgo_utils.errors import ErrorCodes, BaseError, ErrorCategories 9 | 10 | logger = get_logger() 11 | 12 | def event_middleware(event_name: str, func: Callable) -> Callable: 13 | @wraps(func) 14 | async def wrapper(*args, **kwargs) -> Dict[str, Any]: 15 | try: 16 | result = await func(*args, **kwargs) 17 | if not isinstance(result, dict) or not result: 18 | logger.warning(f"Expected result to be a dict, got {type(result)} instead.") 19 | result = {} 20 | 21 | result['status'] = ResponseStatus.SUCCESS.value 22 | return result 23 | 24 | except BaseError as e: 25 | logger.exception(f"Error in {event_name}: {e.error_code.value}", payload=e.to_dict()) 26 | error_code = e.error_code 27 | if error_code.category != ErrorCategories.BUSINESS_LOGIC_ERROR: 28 | error_code = ErrorCodes.UNKNOWN_ERROR 29 | return { 30 | "status": ResponseStatus.FAILURE.value, 31 | "error_code": error_code.value, 32 | } 33 | 34 | except Exception as e: 35 | logger.exception(f"Error in {event_name}: {ErrorCodes.UNKNOWN_ERROR.value}", payload={"error": str(e)}) 36 | return { 37 | "status": ResponseStatus.ERROR.value, 38 | "error_code": ErrorCodes.UNKNOWN_ERROR.value, 39 | } 40 | 41 | return wrapper 42 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.service import ServiceConfig 3 | from config.cache import RedisConfig 4 | from config.db import PostgresConfig 5 | from config.auth import AccountVerificationConfig 6 | from config.enums import LayerNames 7 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/auth.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class AccountVerificationConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | auth_code_ttl_sec: int = None, 7 | auth_code_digits_cnt: int = None, 8 | ): 9 | self.auth_code_ttl_sec = auth_code_ttl_sec or env_var("AUTH_CODE_TTL_SEC", default=420, cast_type=int) 10 | self.auth_code_digits_cnt = auth_code_digits_cnt or env_var("AUTH_CODE_DIGITS_CNT", default=5, cast_type=int) 11 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, TypeVar, Callable 3 | from pydantic import BaseModel, Field 4 | from decouple import config, UndefinedValueError 5 | 6 | T = TypeVar('T') 7 | 8 | 9 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 10 | try: 11 | value = config(field_name, default=default) 12 | if value is None: 13 | return default 14 | return cast_type(value) 15 | except UndefinedValueError: 16 | return default 17 | except (TypeError, ValueError) as e: 18 | if cast_type is None: 19 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 20 | else: 21 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 22 | 23 | 24 | class BaseConfig(): 25 | 26 | @classmethod 27 | def load_environment(cls): 28 | env = config("ENVIRONMENT", default='test') 29 | return env 30 | 31 | def __repr__(self): 32 | class_name = self.__class__.__name__ 33 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 34 | return f'{class_name}({attributes})' 35 | 36 | def dict(self): 37 | return self.__dict__ 38 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5672, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/cache.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class RedisConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | db: int = None, 9 | default_ttl: int = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("REDIS_HOST", "localhost") 13 | self.port = port or env_var("REDIS_PORT", 6490, int) 14 | self.db = db or env_var("REDIS_DB", 0, int) 15 | self.default_ttl = default_ttl or env_var("REDIS_DEFAULT_TTL", 120, int) 16 | self.password = password or env_var("REDIS_PASSWORD", "gateway_password") 17 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | APP = "app" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import BaseConfig, env_var 3 | 4 | class ServiceConfig(BaseConfig): 5 | def __init__( 6 | self, 7 | environment: str = None, 8 | log_level_name: str = None, 9 | ): 10 | self.environment = environment or env_var('ENVIRONMENT', default='test') 11 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 12 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 13 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/restaurant/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from rabbitmq_rpc import RPCClient 5 | 6 | from data_access.repository.cache_repository import CacheRepository 7 | from data_access.repository.db_repository import DatabaseRepository 8 | from data_access.broker import RPCBroker 9 | 10 | from config import BaseConfig 11 | from data_access import get_logger 12 | 13 | async def setup() -> None: 14 | logger = get_logger() 15 | await CacheRepository.initialize() 16 | logger.info("Connected to Redis") 17 | await DatabaseRepository.initialize() 18 | logger.info("Connected to PostgreSQL") 19 | await RPCBroker.initialize(asyncio.get_event_loop()) 20 | logger.info("Connected to RabbitMQ") 21 | 22 | 23 | async def teardown() -> None: 24 | logger = get_logger() 25 | await CacheRepository.terminate() 26 | logger.info("Disconnected from Redis") 27 | await DatabaseRepository.terminate() 28 | logger.info("Disconnected from PostgreSQL") 29 | await RPCBroker.terminate() 30 | logger.info("Disconnected from RabbitMQ") -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/data_access/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.repository.cache_repository import CacheRepository 2 | from data_access.repository.db_repository import DatabaseRepository 3 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/data_access/repository/base.py: -------------------------------------------------------------------------------- 1 | class BaseRepository: 2 | data_access = None 3 | 4 | @classmethod 5 | async def initialize(cls, configuration): 6 | raise NotImplementedError 7 | 8 | @classmethod 9 | async def terminate(cls): 10 | if cls._data_access is not None: 11 | await cls._data_access.disconnect() 12 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DOMAIN.value 6 | 7 | def get_logger(layer: str =layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvloop 3 | from dotenv import load_dotenv 4 | 5 | from ftgo_utils.logger import init_logging 6 | 7 | from config import ServiceConfig 8 | from data_access.events.lifecycle import setup, teardown 9 | from events import register_events 10 | 11 | load_dotenv() 12 | 13 | async def setup_env(): 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def startup_event(): 18 | await setup_env() 19 | await setup() 20 | await asyncio.sleep(1) 21 | await register_events() 22 | await asyncio.Future() 23 | 24 | async def shutdown_event(): 25 | await teardown() 26 | 27 | if __name__ == '__main__': 28 | uvloop.install() 29 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 30 | loop = asyncio.get_event_loop() 31 | 32 | try: 33 | loop.run_until_complete(startup_event()) 34 | finally: 35 | loop.run_until_complete(shutdown_event()) 36 | loop.close() 37 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/models/__init__.py: -------------------------------------------------------------------------------- 1 | from models.supplier import Supplier 2 | from models.menu import MenuItem 3 | from models.base import Base 4 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/models/menu.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func, Float, Integer 2 | from sqlalchemy.orm import relationship 3 | 4 | from ftgo_utils.uuid_gen import uuid4 5 | 6 | from models.base import Base 7 | 8 | class MenuItem(Base): 9 | __tablename__ = "menu_item" 10 | 11 | item_id = Column(String, primary_key=True, default=lambda: uuid4()) 12 | restaurant_id = Column(String, ForeignKey("supplier_profile.id"), nullable=False) 13 | name = Column(String, nullable=False) 14 | price = Column(Float, nullable=False) 15 | count = Column(Integer, nullable=False, default=0) 16 | description = Column(String, nullable=False) 17 | 18 | created_at = Column(DateTime(timezone=True), server_default=func.now()) 19 | updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) 20 | 21 | supplier = relationship("Supplier", back_populates="menu") 22 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/models/supplier.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Float 2 | from sqlalchemy.orm import relationship 3 | from sqlalchemy.sql import func 4 | 5 | from ftgo_utils.enums import Gender, Roles 6 | from ftgo_utils.uuid_gen import uuid4 7 | 8 | from models.base import Base 9 | 10 | class Supplier(Base): 11 | __tablename__ = "supplier_profile" 12 | 13 | id = Column(String, primary_key=True, default=lambda: uuid4()) 14 | owner_user_id = Column(String, nullable=False) 15 | name = Column(String, nullable=False) 16 | postal_code = Column(String, nullable=False) 17 | address = Column(String, nullable=False) 18 | address_lat = Column(Float, nullable=False) 19 | address_lng = Column(Float, nullable=False) 20 | restaurant_licence_id = Column(String, nullable=False) 21 | 22 | created_at = Column(DateTime(timezone=True), server_default=func.now()) 23 | updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) 24 | 25 | menu = relationship("MenuItem", back_populates="supplier", cascade="all, delete-orphan") 26 | 27 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/microservices/restaurant/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/restaurant/tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/restaurant/tests/test_event_sourcerer.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/restaurant/tests/test_event_sourcerer.py -------------------------------------------------------------------------------- /backend/microservices/user/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/.coverage -------------------------------------------------------------------------------- /backend/microservices/user/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | __pycache__ 4 | .coverage 5 | .gitignore 6 | .github 7 | *.md 8 | env 9 | .dockerignore 10 | Dockerfile 11 | Dockerfile.prod 12 | docker-compose.yaml 13 | .eslintrc.js 14 | requirements-build.txt 15 | requirements-lint.txt 16 | requirements-test.txt 17 | requirements-dev.txt 18 | requirements-prod.txt 19 | CONTAINER.md 20 | README.md 21 | node_modules/ -------------------------------------------------------------------------------- /backend/microservices/user/.env: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | LOG_LEVEL=INFO 3 | 4 | ## General Settings 5 | DB_TIMEOUT=5 6 | DB_POOL_SIZE=100 7 | DB_MAX_POOL_CON=80 8 | DB_POOL_OVERFLOW=20 9 | ENABLE_DB_ECHO_LOG=False 10 | ENABLE_DB_EXPIRE_ON_COMMIT=False 11 | ENABLE_DB_FORCE_ROLLBACK=True 12 | -------------------------------------------------------------------------------- /backend/microservices/user/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.9-slim 3 | 4 | # Set environment variables 5 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 6 | ENV ENVIRONMENT=dev 7 | 8 | # Upgrade pip and install Python dependencies 9 | #RUN apt-get update 10 | #RUN apt-get install -y git 11 | RUN apt-get update && \ 12 | apt-get install -y git ca-certificates && \ 13 | apt-get clean && \ 14 | rm -rf /var/lib/apt/lists/* 15 | # Upgrade pip 16 | RUN pip install --upgrade pip 17 | 18 | # Copy requirements.txt before other files to leverage Docker cache 19 | COPY ./requirements.txt /tmp/requirements.txt 20 | 21 | 22 | RUN pip install -r /tmp/requirements.txt --no-cache-dir --force-reinstall 23 | RUN pip install --upgrade --no-deps git+https://github.com/deepmancer/aredis-client.git 24 | RUN pip install --upgrade --no-deps git+https://github.com/deepmancer/asyncpg-client.git 25 | # Copy the application code to the container 26 | COPY . /user 27 | 28 | # Set the working directory 29 | WORKDIR /user 30 | 31 | # Set the PYTHONPATH environment variable 32 | ENV PYTHONPATH=/user/src 33 | 34 | # RUN python -m pytest -v 35 | # Copy the Alembic configuration file 36 | COPY alembic.ini /user/alembic.ini 37 | 38 | # Run Alembic migrations and start the application 39 | CMD alembic upgrade head && python -u src/main.py 40 | -------------------------------------------------------------------------------- /backend/microservices/user/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /backend/microservices/user/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/microservices/user/migrations/versions/1058a7c2c9f9_async_alembic_initial_tables.py: -------------------------------------------------------------------------------- 1 | """async_alembic_initial_tables 2 | 3 | Revision ID: 1058a7c2c9f9 4 | Revises: 5 | Create Date: 2024-07-22 18:35:58.267452 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1058a7c2c9f9' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | def upgrade() -> None: 19 | pass 20 | 21 | def downgrade() -> None: 22 | pass 23 | -------------------------------------------------------------------------------- /backend/microservices/user/migrations/versions/a227d3acc2ff_async_alembic_initial_tables.py: -------------------------------------------------------------------------------- 1 | """async_alembic_initial_tables 2 | 3 | Revision ID: a227d3acc2ff 4 | Revises: d9a4133406b7 5 | Create Date: 2024-07-22 18:41:05.599738 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a227d3acc2ff' 14 | down_revision = 'd9a4133406b7' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | pass 21 | 22 | 23 | def downgrade() -> None: 24 | pass 25 | -------------------------------------------------------------------------------- /backend/microservices/user/migrations/versions/c457ed863290_async_alembic_initial_tables.py: -------------------------------------------------------------------------------- 1 | """async_alembic_initial_tables 2 | 3 | Revision ID: c457ed863290 4 | Revises: 1058a7c2c9f9 5 | Create Date: 2024-07-22 18:39:17.248554 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c457ed863290' 14 | down_revision = '1058a7c2c9f9' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | pass 21 | 22 | 23 | def downgrade() -> None: 24 | pass 25 | -------------------------------------------------------------------------------- /backend/microservices/user/migrations/versions/d9a4133406b7_async_alembic_initial_tables.py: -------------------------------------------------------------------------------- 1 | """async_alembic_initial_tables 2 | 3 | Revision ID: d9a4133406b7 4 | Revises: c457ed863290 5 | Create Date: 2024-07-22 18:39:29.200298 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd9a4133406b7' 14 | down_revision = 'c457ed863290' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | pass 21 | 22 | 23 | def downgrade() -> None: 24 | pass 25 | -------------------------------------------------------------------------------- /backend/microservices/user/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = 3 | test_*.py 4 | *_test.py 5 | 6 | pythonpath = src tests 7 | 8 | testpaths = 9 | tests 10 | -------------------------------------------------------------------------------- /backend/microservices/user/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.8.1 2 | asgi_lifespan 3 | asyncio 4 | asyncpg 5 | black 6 | docker 7 | dataclasses 8 | freezegun 9 | greenlet 10 | loguru 11 | mypy 12 | psycopg2-binary 13 | pytest 14 | pytest-asyncio 15 | python-decouple 16 | python-dotenv 17 | SQLAlchemy==2.0.0b3 18 | sqlalchemy 19 | pytz 20 | trio 21 | uvloop 22 | pytest 23 | pytest-asyncio 24 | pytest-cov 25 | git+https://github.com/deepmancer/ftgo-utils.git 26 | git+https://github.com/deepmancer/aredis-client.git 27 | git+https://github.com/deepmancer/asyncpg-client.git 28 | git+https://github.com/deepmancer/rabbitmq-rpc.git -------------------------------------------------------------------------------- /backend/microservices/user/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/src/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/src/application/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.APP.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | from application.vehicle import VehicleService 11 | from application.address import AddressService 12 | from application.profile import ProfileService 13 | -------------------------------------------------------------------------------- /backend/microservices/user/src/application/vehicle.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from domain import UserManager 4 | 5 | class VehicleService: 6 | @staticmethod 7 | async def register_vehicle(user_id: str, plate_number: str, license_number: str, **kwargs) -> Dict[str, Any]: 8 | driver = await UserManager.load(user_id) 9 | vehicle_info = await driver.register_vehicle( 10 | plate_number=plate_number, 11 | license_number=license_number, 12 | ) 13 | return vehicle_info 14 | 15 | @staticmethod 16 | async def get_vehicle_info(user_id: str, **kwargs) -> Dict[str, Any]: 17 | driver = await UserManager.load(user_id) 18 | vehicle_info = driver.get_vehicle_info() 19 | return vehicle_info 20 | 21 | @staticmethod 22 | async def delete_vehicle(user_id: str, **kwargs) -> Dict[str, Any]: 23 | driver = await UserManager.load(user_id) 24 | vehicle_id = await driver.delete_vehicle() 25 | return {"vehicle_id": vehicle_id} 26 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | from config.service import ServiceConfig 3 | from config.cache import RedisConfig 4 | from config.db import PostgresConfig 5 | from config.auth import AccountVerificationConfig 6 | from config.enums import LayerNames 7 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/auth.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class AccountVerificationConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | auth_code_ttl_sec: int = None, 7 | auth_code_digits_cnt: int = None, 8 | ): 9 | self.auth_code_ttl_sec = auth_code_ttl_sec or env_var("AUTH_CODE_TTL_SEC", default=420, cast_type=int) 10 | self.auth_code_digits_cnt = auth_code_digits_cnt or env_var("AUTH_CODE_DIGITS_CNT", default=5, cast_type=int) 11 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, TypeVar, Callable 3 | from decouple import config, UndefinedValueError 4 | 5 | T = TypeVar('T') 6 | 7 | def env_var(field_name: str, default: Any = None, cast_type: Callable[[str], T] = str) -> T: 8 | try: 9 | value = config(field_name, default=default) 10 | if value is None: 11 | return default 12 | return cast_type(value) 13 | except UndefinedValueError: 14 | return default 15 | except (TypeError, ValueError) as e: 16 | if cast_type is None: 17 | raise ValueError(f"Failed to cast environment variable {field_name} to {str.__name__}") from e 18 | else: 19 | raise ValueError(f"Failed to cast environment variable {field_name} to {cast_type.__name__}") from e 20 | 21 | class BaseConfig(): 22 | 23 | @classmethod 24 | def load_environment(cls): 25 | env = config("ENVIRONMENT", default='test') 26 | return env 27 | 28 | def __repr__(self): 29 | class_name = self.__class__.__name__ 30 | attributes = ', '.join(f'{key}={value!r}' for key, value in self.__dict__.items()) 31 | return f'{class_name}({attributes})' 32 | 33 | def dict(self): 34 | return self.__dict__ 35 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/broker.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class BrokerConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | user: str = None, 9 | password: str = None, 10 | vhost: str = None, 11 | ): 12 | self.host = host or env_var("RABBITMQ_HOST", default="localhost") 13 | self.port = port or env_var("RABBITMQ_PORT", default=5672, cast_type=int) 14 | self.user = user or env_var("RABBITMQ_USER", default="rabbitmq_user") 15 | self.password = password or env_var("RABBITMQ_PASS", default="rabbitmq_password") 16 | self.vhost = vhost or env_var("RABBITMQ_VHOST", default="/") 17 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/cache.py: -------------------------------------------------------------------------------- 1 | from config.base import BaseConfig, env_var 2 | 3 | class RedisConfig(BaseConfig): 4 | def __init__( 5 | self, 6 | host: str = None, 7 | port: int = None, 8 | db: int = None, 9 | default_ttl: int = None, 10 | password: str = None, 11 | ): 12 | self.host = host or env_var("REDIS_HOST", "localhost") 13 | self.port = port or env_var("REDIS_PORT", 6235, int) 14 | self.db = db or env_var("REDIS_DB", 0, int) 15 | self.default_ttl = default_ttl or env_var("REDIS_DEFAULT_TTL", 120, int) 16 | self.password = password or env_var("REDIS_PASSWORD", "user_password") 17 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class LayerNames(str, enum.Enum): 4 | APP = "app" 5 | DOMAIN = "domain" 6 | DATA_ACCESS = "data_access" 7 | MESSAGE_BROKER = "message_broker" 8 | -------------------------------------------------------------------------------- /backend/microservices/user/src/config/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import BaseConfig, env_var 3 | 4 | class ServiceConfig(BaseConfig): 5 | def __init__( 6 | self, 7 | environment: str = None, 8 | log_level_name: str = None, 9 | ): 10 | self.environment = environment or env_var('ENVIRONMENT', default='test') 11 | self.log_level_name = log_level_name or env_var('LOG_LEVEL', default='INFO') 12 | self.log_level = logging._nameToLevel.get(self.log_level_name, logging.DEBUG) 13 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DATA_ACCESS.value 6 | 7 | def get_logger(layer: str = layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/src/data_access/events/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/events/lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from rabbitmq_rpc import RPCClient 5 | 6 | from data_access.repository.cache_repository import CacheRepository 7 | from data_access.repository.db_repository import DatabaseRepository 8 | from data_access.broker import RPCBroker 9 | 10 | from config import BaseConfig 11 | from data_access import get_logger 12 | 13 | async def setup() -> None: 14 | logger = get_logger() 15 | await CacheRepository.initialize() 16 | logger.info("Connected to Redis") 17 | await DatabaseRepository.initialize() 18 | logger.info("Connected to PostgreSQL") 19 | await RPCBroker.initialize(asyncio.get_event_loop()) 20 | logger.info("Connected to RabbitMQ") 21 | 22 | 23 | async def teardown() -> None: 24 | logger = get_logger() 25 | await CacheRepository.terminate() 26 | logger.info("Disconnected from Redis") 27 | await DatabaseRepository.terminate() 28 | logger.info("Disconnected from PostgreSQL") 29 | await RPCBroker.terminate() 30 | logger.info("Disconnected from RabbitMQ") -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/models/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.models.address import Address 2 | from data_access.models.profile import Profile 3 | from data_access.models.vehicle import VehicleInfo 4 | from data_access.models.base import Base 5 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/models/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Type, Any 3 | 4 | import sqlalchemy 5 | from sqlalchemy import DateTime, func 6 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 7 | 8 | from ftgo_utils.uuid_gen import uuid4 9 | 10 | from dto import BaseDTO 11 | 12 | class Base(DeclarativeBase): 13 | __abstract__ = True 14 | metadata = sqlalchemy.MetaData() 15 | 16 | id: Mapped[str] = mapped_column( 17 | sqlalchemy.String, primary_key=True, default=uuid4 18 | ) 19 | created_at: Mapped[datetime] = mapped_column( 20 | DateTime(timezone=True), server_default=func.now() 21 | ) 22 | updated_at: Mapped[datetime] = mapped_column( 23 | DateTime(timezone=True), server_default=func.now(), onupdate=func.now() 24 | ) 25 | 26 | def to_dict(self) -> Dict[str, Any]: 27 | return {column.name: getattr(self, column.name) for column in self.__table__.columns} 28 | 29 | def __repr__(self) -> str: 30 | return f"<{self.__class__.__name__}({self.to_dict()})>" 31 | 32 | @classmethod 33 | def from_dto(cls, dto: BaseDTO) -> 'Base': 34 | raise NotImplementedError 35 | 36 | def to_dto(self) -> BaseDTO: 37 | raise NotImplementedError 38 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/models/vehicle.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import String, DateTime, ForeignKey 2 | from sqlalchemy.orm import relationship, mapped_column, Mapped 3 | 4 | from data_access.models.base import Base 5 | from dto import VehicleDTO 6 | 7 | class VehicleInfo(Base): 8 | __tablename__ = "vehicle_info" 9 | 10 | driver_id: Mapped[str] = mapped_column(String, ForeignKey("user_profile.id"), nullable=False) 11 | plate_number: Mapped[str] = mapped_column(String, nullable=False) 12 | license_number: Mapped[str] = mapped_column(String, nullable=False) 13 | 14 | driver: Mapped["Profile"] = relationship("Profile", back_populates="vehicle_info") 15 | 16 | @classmethod 17 | def from_dto(cls, dto: VehicleDTO) -> 'VehicleInfo': 18 | return cls( 19 | id=dto.vehicle_id, 20 | driver_id=dto.driver_id, 21 | plate_number=dto.plate_number, 22 | license_number=dto.license_number, 23 | ) 24 | 25 | def to_dto(self) -> VehicleDTO: 26 | return VehicleDTO( 27 | vehicle_id=self.id, 28 | driver_id=self.driver_id, 29 | plate_number=self.plate_number, 30 | license_number=self.license_number, 31 | created_at=self.created_at, 32 | ) 33 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from data_access.repository.cache_repository import CacheRepository 2 | from data_access.repository.db_repository import DatabaseRepository 3 | -------------------------------------------------------------------------------- /backend/microservices/user/src/data_access/repository/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class BaseRepository: 5 | _data_access = None 6 | 7 | @classmethod 8 | async def insert(cls, *args, **kwargs) -> Any: 9 | raise NotImplementedError 10 | 11 | @classmethod 12 | async def delete(cls, *args, **kwargs) -> Any: 13 | raise NotImplementedError 14 | 15 | @classmethod 16 | async def update(cls, *args, **kwargs) -> Any: 17 | raise NotImplementedError 18 | 19 | @classmethod 20 | async def fetch(cls, *args, **kwargs) -> Any: 21 | raise NotImplementedError 22 | 23 | @classmethod 24 | async def initialize(cls) -> None: 25 | raise NotImplementedError 26 | 27 | @classmethod 28 | async def terminate(cls) -> None: 29 | if cls._data_access: 30 | await cls._data_access.disconnect() 31 | cls._data_access = None 32 | -------------------------------------------------------------------------------- /backend/microservices/user/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | from ftgo_utils.logger import get_logger as _get_logger 2 | 3 | from config import ServiceConfig, LayerNames 4 | 5 | layer = LayerNames.DOMAIN.value 6 | 7 | def get_logger(layer: str =layer): 8 | return _get_logger(layer=layer, env=ServiceConfig.load_environment()) 9 | 10 | from domain.manager import UserManager 11 | -------------------------------------------------------------------------------- /backend/microservices/user/src/domain/assets/__init__.py: -------------------------------------------------------------------------------- 1 | from domain.assets.address import AddressDomain 2 | from domain.assets.vehicle import VehicleDomain 3 | -------------------------------------------------------------------------------- /backend/microservices/user/src/domain/authentication.py: -------------------------------------------------------------------------------- 1 | import pyotp 2 | 3 | from config import AccountVerificationConfig 4 | 5 | auth_config = AccountVerificationConfig() 6 | class Authenticator: 7 | _otp = pyotp.TOTP( 8 | pyotp.random_base32(), 9 | digits=auth_config.auth_code_digits_cnt, 10 | interval=auth_config.auth_code_ttl_sec, 11 | ) 12 | 13 | @staticmethod 14 | def create_auth_code(user_id: str) -> str: 15 | auth_code = Authenticator._otp.now() 16 | return auth_code, Authenticator._otp.interval 17 | 18 | @staticmethod 19 | def verify_auth_code(auth_code: str) -> bool: 20 | return Authenticator._otp.verify(auth_code) 21 | -------------------------------------------------------------------------------- /backend/microservices/user/src/dto/__init__.py: -------------------------------------------------------------------------------- 1 | from dto.base import BaseDTO 2 | from dto.address import AddressDTO 3 | from dto.profile import ProfileDTO 4 | from dto.vehicle import VehicleDTO 5 | -------------------------------------------------------------------------------- /backend/microservices/user/src/dto/address.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from dataclasses import dataclass 3 | 4 | from dto.base import BaseDTO 5 | 6 | @dataclass 7 | class AddressDTO(BaseDTO): 8 | address_id: Optional[str] = None 9 | user_id: Optional[str] = None 10 | latitude: Optional[float] = None 11 | longitude: Optional[float] = None 12 | address_line_1: Optional[str] = None 13 | address_line_2: Optional[str] = None 14 | city: Optional[str] = None 15 | postal_code: Optional[str] = None 16 | country: Optional[str] = None 17 | is_default: Optional[bool] = False 18 | -------------------------------------------------------------------------------- /backend/microservices/user/src/dto/base.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from datetime import datetime 3 | from dataclasses import dataclass, asdict 4 | 5 | 6 | @dataclass 7 | class BaseDTO: 8 | created_at: Optional[datetime] = None 9 | 10 | def to_dict(self): 11 | return asdict(self) 12 | -------------------------------------------------------------------------------- /backend/microservices/user/src/dto/profile.py: -------------------------------------------------------------------------------- 1 | import email 2 | from typing import Optional 3 | from datetime import datetime 4 | from dataclasses import dataclass 5 | 6 | from dto.base import BaseDTO 7 | 8 | @dataclass 9 | class ProfileDTO(BaseDTO): 10 | user_id: Optional[str] = None 11 | phone_number: Optional[str] = None 12 | hashed_password: Optional[str] = None 13 | first_name: Optional[str] = None 14 | last_name: Optional[str] = None 15 | gender: Optional[str] = None 16 | email: Optional[email] = None 17 | role: Optional[str] = None 18 | national_id: Optional[str] = None 19 | verified_at: Optional[datetime] = None 20 | last_login_time: Optional[datetime] = None 21 | -------------------------------------------------------------------------------- /backend/microservices/user/src/dto/vehicle.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from dataclasses import dataclass 3 | 4 | from dto.base import BaseDTO 5 | 6 | @dataclass 7 | class VehicleDTO(BaseDTO): 8 | vehicle_id: Optional[str] = None 9 | driver_id: Optional[str] = None 10 | plate_number: Optional[str] = None 11 | license_number: Optional[str] = None 12 | -------------------------------------------------------------------------------- /backend/microservices/user/src/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvloop 3 | from dotenv import load_dotenv 4 | 5 | from ftgo_utils.logger import init_logging 6 | 7 | from config import ServiceConfig 8 | from data_access.events.lifecycle import setup, teardown 9 | from events import register_events 10 | 11 | load_dotenv() 12 | 13 | async def setup_env(): 14 | service_config = ServiceConfig() 15 | init_logging(level=service_config.log_level) 16 | 17 | async def startup_event(): 18 | await setup_env() 19 | await setup() 20 | await asyncio.sleep(1) 21 | await register_events() 22 | await asyncio.Future() 23 | 24 | async def shutdown_event(): 25 | await teardown() 26 | 27 | if __name__ == '__main__': 28 | uvloop.install() 29 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 30 | loop = asyncio.get_event_loop() 31 | 32 | try: 33 | loop.run_until_complete(startup_event()) 34 | finally: 35 | loop.run_until_complete(shutdown_event()) 36 | loop.close() 37 | -------------------------------------------------------------------------------- /backend/microservices/user/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils.exception import handle_exception 2 | -------------------------------------------------------------------------------- /backend/microservices/user/src/utils/exception.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | 3 | from ftgo_utils.errors import BaseError, ErrorCodes, ErrorCode 4 | 5 | 6 | async def handle_exception( 7 | e: Exception, 8 | error_code: Optional[ErrorCode] = None, 9 | payload: Optional[Dict[str, Any]] = None, 10 | message: Optional[str] = None, 11 | **kwargs, 12 | ) -> None: 13 | if isinstance(e, BaseError): 14 | raise e 15 | else: 16 | updated_payload = payload.copy() if payload else {} 17 | updated_payload.update(kwargs) 18 | base_exc = BaseError( 19 | error_code=error_code if error_code else ErrorCodes.UNKNOWN_ERROR, 20 | message=message or str(e), 21 | payload=updated_payload, 22 | ) 23 | raise base_exc from e 24 | -------------------------------------------------------------------------------- /backend/microservices/user/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/component_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/component_test/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/component_test/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/component_test/application/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import pytest_asyncio 4 | 5 | from test_doubles.redis import FakeAsyncRedis 6 | from test_doubles.time import TimeProvider 7 | from data_access.repository.cache_repository import CacheRepository 8 | 9 | MOCKED_TIMESTAMP = 1704067200 # 2024, January 1 10 | 11 | @pytest.fixture(scope='function') 12 | def time_machine(): 13 | provider = TimeProvider(timestamp=MOCKED_TIMESTAMP) 14 | provider.start() 15 | yield provider 16 | provider.stop() 17 | 18 | @pytest_asyncio.fixture(scope='function') 19 | async def fake_redis(time_machine: TimeProvider): 20 | redis = await FakeAsyncRedis.create( 21 | host="localhost", 22 | port=6379, 23 | db=0, 24 | time_provider=time_machine.current_timestamp 25 | ) 26 | return redis 27 | 28 | @pytest_asyncio.fixture(scope='function') 29 | async def cache_repository(fake_redis): 30 | CacheRepository._data_access = fake_redis 31 | yield CacheRepository 32 | 33 | @pytest_asyncio.fixture 34 | async def setup_and_teardown_cache(cache_repository): 35 | async with cache_repository._data_access.get_or_create_session() as session: 36 | await session.flushdb() 37 | yield 38 | async with cache_repository._data_access.get_or_create_session() as session: 39 | await session.flushdb() 40 | -------------------------------------------------------------------------------- /backend/microservices/user/tests/test_doubles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/test_doubles/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/test_doubles/time.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from typing import Optional 4 | 5 | from freezegun import freeze_time 6 | 7 | 8 | class TimeProvider: 9 | def __init__(self, timestamp: float) -> None: 10 | self._frozen_time_cls = freeze_time(datetime.datetime.fromtimestamp(timestamp)) 11 | self._frozen_time = None 12 | 13 | def start(self) -> freeze_time: 14 | self._frozen_time = self._frozen_time_cls.start() 15 | return self._frozen_time 16 | 17 | def advance_time(self, seconds: int) -> None: 18 | if self._frozen_time is not None: 19 | self._frozen_time.tick(datetime.timedelta(seconds=seconds+1e-2)) 20 | 21 | def stop(self) -> None: 22 | if self._frozen_time is not None: 23 | self._frozen_time_cls.stop() 24 | self._frozen_time = None 25 | 26 | def current_timestamp(self) -> Optional[float]: 27 | if self._frozen_time is not None: 28 | return self._frozen_time.time_to_freeze.timestamp() 29 | return None 30 | 31 | def current_datetime(self) -> Optional[datetime.datetime]: 32 | if self._frozen_time is not None: 33 | return self._frozen_time.time_to_freeze 34 | return None 35 | -------------------------------------------------------------------------------- /backend/microservices/user/tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/unit_tests/data_access/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/unit_tests/data_access/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/unit_tests/data_access/repository/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/unit_tests/data_access/repository/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/unit_tests/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/unit_tests/domain/__init__.py -------------------------------------------------------------------------------- /backend/microservices/user/tests/unit_tests/domain/assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/backend/microservices/user/tests/unit_tests/domain/assets/__init__.py -------------------------------------------------------------------------------- /backend/start_services.sh: -------------------------------------------------------------------------------- 1 | start_infra_services() { 2 | docker-compose -f infra/docker-compose.yaml up -d 3 | sleep 8 4 | } 5 | 6 | start_backend_services() { 7 | docker-compose up --build 8 | } 9 | 10 | start_infra_services 11 | start_backend_services 12 | 13 | echo "Backend services are up and running." -------------------------------------------------------------------------------- /backend/stop_services.sh: -------------------------------------------------------------------------------- 1 | stop_containers() { 2 | echo "Stopping containers and removing orphans..." 3 | docker-compose down --remove-orphans 4 | sleep 0.1 5 | docker stop `docker ps -qa` 6 | sleep 0.1 7 | docker rm -f `docker ps -qa` 8 | sleep 0.1 9 | sudo systemctl restart docker.socket docker.service docker 10 | } 11 | 12 | free_ports() { 13 | ports=(15920 5920 6490 5438 6235 8000 5020 5920 5540 8888 9090 3000 6300 5439 15673 5673 5441 8081 8080 28081 27017 7017 8085 3030) 14 | for port in "${ports[@]}"; do 15 | echo "Freeing up port $port..." 16 | sudo kill -9 $(sudo lsof -t -i:$port) 2>/dev/null 17 | sleep 0.025 18 | done 19 | } 20 | 21 | recreate_network() { 22 | echo "Recreating network..." 23 | docker network rm backend-network 24 | docker network rm frontend-network 25 | sleep 0.1 26 | docker network create --driver bridge backend-network 27 | docker network create --driver bridge frontend-network 28 | } 29 | 30 | remove_volumes() { 31 | echo "Removing volumes..." 32 | docker volume rm $(docker volume ls -q | grep "^backend_") 33 | } 34 | 35 | stop_containers 36 | free_ports 37 | recreate_network 38 | remove_volumes 39 | 40 | echo "Backend services are stopped gracefully." 41 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # FTGO 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | } -------------------------------------------------------------------------------- /ui/images/McDonalds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/images/McDonalds.png -------------------------------------------------------------------------------- /ui/images/Restaurant-Register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/images/Restaurant-Register.png -------------------------------------------------------------------------------- /ui/images/signin-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/images/signin-image.jpg -------------------------------------------------------------------------------- /ui/images/signup-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/images/signup-image.jpg -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FTGO", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve", 7 | "build": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build", 8 | "lint": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-free": "^6.5.2", 12 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 13 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 14 | "@fortawesome/vue-fontawesome": "^2.0.10", 15 | "aws-sdk": "^2.1430.0", 16 | "bootstrap": "^4.5.3", 17 | "bootstrap-vue": "^2.23.1", 18 | "core-js": "^3.6.5", 19 | "vue": "^2.7.14", 20 | "vue-axios": "^3.5.2", 21 | "vue-router": "^3.2.0", 22 | "vuex": "^3.6.2" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.5.15", 26 | "@vue/cli-plugin-eslint": "~4.5.15", 27 | "@vue/cli-plugin-router": "~4.5.15", 28 | "@vue/cli-plugin-vuex": "~4.5.15", 29 | "@vue/cli-service": "~4.5.15", 30 | "babel-eslint": "^10.1.0", 31 | "eslint": "^6.7.2", 32 | "eslint-plugin-vue": "^6.2.2", 33 | "vue-template-compiler": "^2.6.11" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/images/McDonalds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/images/McDonalds.png -------------------------------------------------------------------------------- /ui/public/images/UserMainPage-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/images/UserMainPage-image.jpg -------------------------------------------------------------------------------- /ui/public/images/UserMainPage-image.jpgZone.Identifier: -------------------------------------------------------------------------------- 1 | [ZoneTransfer] 2 | ZoneId=3 3 | ReferrerUrl=https://www.google.com/ 4 | HostUrl=https://t4.ftcdn.net/jpg/02/94/21/87/360_F_294218701_se4mQtVmQoPnG4UX7J8PjvTzn8yeWyqF.jpg 5 | -------------------------------------------------------------------------------- /ui/public/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/images/background.jpg -------------------------------------------------------------------------------- /ui/public/images/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/images/default-avatar.png -------------------------------------------------------------------------------- /ui/public/images/kebab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepmancer/full-stack-fastapi-ftgo/52b1fd1b5d808e32b7925e890f560445a8460e7a/ui/public/images/kebab.png -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |