diff --git a/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml index c35be604..766f09bf 100644 --- a/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml +++ b/.github/workflows/time-tracker-v1-on-pull-request-workflow.yml @@ -26,7 +26,6 @@ jobs: python -m pip install --upgrade pip pip install -r requirements/time_tracker_api/dev.txt pip install -r requirements/time_tracker_events/dev.txt - - name: Login to azure uses: Azure/login@v1 with: @@ -54,7 +53,6 @@ jobs: AZURE_STORAGE_ACCOUNT_KEY: ${{ steps.timeTrackerAzureVault.outputs.AZURE-STORAGE-ACCOUNT-KEY }} run: | pytest tests - - name: Test the build of the app run: | - docker build . + docker build . \ No newline at end of file diff --git a/.github/workflows/time-tracker-v1-on-push-workflow.yml b/.github/workflows/time-tracker-v1-on-push-workflow.yml index 152998b4..095712b9 100644 --- a/.github/workflows/time-tracker-v1-on-push-workflow.yml +++ b/.github/workflows/time-tracker-v1-on-push-workflow.yml @@ -26,7 +26,6 @@ jobs: python -m pip install --upgrade pip pip install -r requirements/time_tracker_api/dev.txt pip install -r requirements/time_tracker_events/dev.txt - - name: Login to azure uses: Azure/login@v1 with: @@ -54,7 +53,6 @@ jobs: AZURE_STORAGE_ACCOUNT_KEY: ${{ steps.timeTrackerAzureVault.outputs.AZURE-STORAGE-ACCOUNT-KEY }} run: | pytest tests - - name: Login to docker registry uses: azure/docker-login@v1 with: @@ -64,4 +62,4 @@ jobs: - name: Build and push image run: | docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} - docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} + docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/timetrackerapi:${{ github.sha }} \ No newline at end of file diff --git a/V2/serverless.yml b/V2/serverless.yml index 9b31ee0b..f7ae5fd7 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -36,7 +36,12 @@ package: - '!.gitignore' - '!.git/**' +#region start Functions + functions: + +#region Start Functions Activities + get_activities: handler: time_tracker/activities/interface.get_activities events: @@ -77,6 +82,10 @@ functions: route: activities/ authLevel: anonymous +#endregion End Functions Activities + +#region Start Functions Time-Entries + create_time_entry: handler: time_tracker/time_entries/interface.create_time_entry events: @@ -127,6 +136,10 @@ functions: route: time-entries/latest/ authLevel: anonymous +#endregion End Functions Time-Entries + +#region Start Functions Customers + create_customer: handler: time_tracker/customers/interface.create_customer events: @@ -137,6 +150,40 @@ functions: route: customers/ authLevel: anonymous + get_customers: + handler: time_tracker/customers/interface.get_customers + events: + - http: true + x-azure-settings: + methods: + - GET + route: customers/{id:?} + authLevel: anonymous + + update_customer: + handler: time_tracker/customers/interface.update_customer + events: + - http: true + x-azure-settings: + methods: + - PUT + route: customers/{id} + authLevel: anonymous + + delete_customer: + handler: time_tracker/customers/interface.delete_customer + events: + - http: true + x-azure-settings: + methods: + - DELETE + route: customers/{id} + authLevel: anonymous + +#endregion End Functions Customers + +#region Start Functions Projects + get_projects: handler: time_tracker/projects/interface.get_projects events: @@ -177,3 +224,7 @@ functions: route: projects/ authLevel: anonymous + +#endregion End Functions Projects + +#endregion End Functions \ No newline at end of file diff --git a/V2/tests/api/azure/customer_azure_endpoints_test.py b/V2/tests/api/azure/customer_azure_endpoints_test.py index 47a619d5..f1f35d4f 100644 --- a/V2/tests/api/azure/customer_azure_endpoints_test.py +++ b/V2/tests/api/azure/customer_azure_endpoints_test.py @@ -1,3 +1,4 @@ +from http import HTTPStatus import json from faker import Faker @@ -8,7 +9,7 @@ CUSTOMER_URL = "/api/customers/" -def test__customer_azure_endpoint__creates_a_customer__when_customer_has_all_necesary_attributes( +def test__create_customer_azure_endpoint__creates_a_customer__when_customer_has_all_necesary_attributes( customer_factory ): customer_body = customer_factory().__dict__ @@ -24,11 +25,11 @@ def test__customer_azure_endpoint__creates_a_customer__when_customer_has_all_nec customer_json_data = json.loads(response.get_body()) customer_body['id'] = customer_json_data['id'] - assert response.status_code == 201 + assert response.status_code == HTTPStatus.CREATED assert customer_json_data == customer_body -def test__customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_necessary_attributes(): +def test__create_customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_necessary_attributes(): customer_to_insert = { "id": None, "name": Faker().user_name(), @@ -45,5 +46,169 @@ def test__customer_azure_endpoint__returns_a_status_400__when_dont_recieve_all_n response = azure_customers._create_customer.create_customer(req) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST assert response.get_body() == b'Invalid format or structure of the attributes of the customer' + + +def test__delete_customer_azure_endpoint__returns_a_customer_with_true_deleted__when_its_id_is_found( + test_db, customer_factory, insert_customer +): + customer_preinsert = customer_factory() + inserted_customer = insert_customer(customer_preinsert, test_db).__dict__ + + req = func.HttpRequest( + method='DELETE', + body=None, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + customer_json_data = json.loads(response.get_body().decode("utf-8")) + + assert response.status_code == HTTPStatus.OK + assert customer_json_data['deleted'] is True + + +def test__delete_customer_azure_endpoint__returns_not_found__when_its_id_is_not_found(): + req = func.HttpRequest( + method='DELETE', + body=None, + url=CUSTOMER_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.get_body() == b'Not found' + + +def test__update_customer_azure_endpoint__returns_an_updated_customer__when_customer_has_all_necesary_attributes( + test_db, customer_factory, insert_customer +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + inserted_customer["description"] = Faker().sentence() + + body = json.dumps(inserted_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._update_customer.update_customer(req) + customer_json_data = json.loads(response.get_body()) + + assert response.status_code == HTTPStatus.OK + assert customer_json_data == inserted_customer + + +def test__update_customer_azure_endpoint__returns_update_a_customer__when_customer_has_all_necesary_attributes( + customer_factory +): + existent_customer = customer_factory().__dict__ + + body = json.dumps(existent_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": Faker().pyint()}, + ) + + response = azure_customers._update_customer.update_customer(req) + + assert response.status_code == HTTPStatus.CONFLICT + assert response.get_body() == b'This customer does not exist or is duplicated' + + +def test__update_customer_azure_endpoint__returns_invalid_format__when_customer_doesnt_have_all_necesary_attributes( + customer_factory, insert_customer, test_db +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + inserted_customer.pop("name") + + body = json.dumps(inserted_customer).encode("utf-8") + req = func.HttpRequest( + method='PUT', + body=body, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._update_customer.update_customer(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'Invalid format or structure of the attributes of the customer' + + +def test__delete_customers_azure_endpoint__returns_a_status_code_400__when_customer_recive_invalid_id( +): + req = func.HttpRequest( + method="DELETE", + body=None, + url=CUSTOMER_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_customers._delete_customer.delete_customer(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'Invalid Format ID' + + +def test__customers_azure_endpoint__returns_all_customers( + test_db, customer_factory, insert_customer +): + customer_to_insert = customer_factory() + + inserted_customer = insert_customer(customer_to_insert, test_db).__dict__ + + req = func.HttpRequest(method='GET', body=None, url=CUSTOMER_URL) + response = azure_customers._get_customers.get_customers(req) + customers_json_data = response.get_body().decode("utf-8") + customer_list = json.loads(customers_json_data) + + assert response.status_code == HTTPStatus.OK + assert customers_json_data <= json.dumps(inserted_customer) + assert customer_list.pop() == inserted_customer + + +def test__customer_azure_endpoint__returns_a_customer__when_customer_matches_its_id( + test_db, customer_factory, insert_customer +): + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, test_db).__dict__ + + req = func.HttpRequest( + method='GET', + body=None, + url=CUSTOMER_URL, + route_params={"id": inserted_customer["id"]}, + ) + + response = azure_customers._get_customers.get_customers(req) + customer_json_data = response.get_body().decode("utf-8") + + assert response.status_code == HTTPStatus.OK + assert customer_json_data == json.dumps(inserted_customer) + + +def test__customer_azure_endpoint__returns_invalid_id__when_customer_not_matches_its_id(): + req = func.HttpRequest( + method='GET', + body=None, + url=CUSTOMER_URL, + route_params={"id": "Invalid ID"}, + ) + + response = azure_customers._get_customers.get_customers(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'The id has an invalid format' diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index 2eae7b16..e4e52fb3 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -2,9 +2,9 @@ from faker import Faker import time_tracker.activities._domain as activities_domain -import time_tracker.activities._infrastructure as activities_infrastructure import time_tracker.time_entries._domain as time_entries_domain import time_tracker.customers._domain as customers_domain +import time_tracker.activities._infrastructure as activities_infrastructure import time_tracker.customers._infrastructure as customers_infrastructure import time_tracker.projects._domain as projects_domain from time_tracker._infrastructure import DB diff --git a/V2/tests/integration/daos/customers_dao_test.py b/V2/tests/integration/daos/customers_dao_test.py index b85cd3e3..496aaf47 100644 --- a/V2/tests/integration/daos/customers_dao_test.py +++ b/V2/tests/integration/daos/customers_dao_test.py @@ -1,4 +1,7 @@ +import typing + import pytest +from faker import Faker import time_tracker.customers._domain as domain import time_tracker.customers._infrastructure as infrastructure @@ -22,7 +25,7 @@ def _clean_database(): dao.db.get_session().execute(query) -def test__customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_database( +def test__create_customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_database( test_db, customer_factory, create_fake_dao ): dao = create_fake_dao(test_db) @@ -33,3 +36,104 @@ def test__customer_dao__returns_a_customer_dto__when_saves_correctly_with_sql_da assert isinstance(inserted_customer, domain.Customer) assert inserted_customer == customer_to_insert + + +def test__get_all__returns_a_list_of_customer_dto_objects__when_one_or_more_customers_are_found_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + customer_to_insert = customer_factory() + inserted_customer = [dao.create(customer_to_insert)] + + customers = dao.get_all() + + assert isinstance(customers, typing.List) + assert customers == inserted_customer + + +def test_get_by_id__returns_a_customer_dto__when_found_one_customer_that_matches_its_id_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db) + + customer = dao.get_by_id(inserted_customer.id) + + assert isinstance(customer, domain.Customer) + assert customer.id == inserted_customer.id + assert customer == inserted_customer + + +def test__get_by_id__returns_none__when_no_customer_matches_its_id_with_sql_database( + test_db, create_fake_dao, customer_factory +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + customer = dao.get_by_id(existent_customer.id) + + assert customer is None + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_customers_with_sql_database( + test_db, create_fake_dao +): + customers = create_fake_dao(test_db).get_all() + + assert isinstance(customers, typing.List) + assert customers == [] + + +def test_delete__returns_a_customer_with_inactive_status__when_a_customer_matching_its_id_is_found_with_sql_database( + test_db, create_fake_dao, customer_factory, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db) + + customer = dao.delete(inserted_customer.id) + + assert isinstance(customer, domain.Customer) + assert customer.id == inserted_customer.id + assert customer.status == 1 + assert customer.deleted is True + + +def test_delete__returns_none__when_no_customer_matching_its_id_is_found_with_sql_database( + test_db, create_fake_dao, customer_factory +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + results = dao.delete(existent_customer.id) + + assert results is None + + +def test__update_customer_dao__returns_an_updated_customer_dto__when_updates_correctly_with_sql_database( + test_db, customer_factory, create_fake_dao, insert_customer +): + dao = create_fake_dao(test_db) + + existent_customer = customer_factory() + inserted_customer = insert_customer(existent_customer, dao.db).__dict__ + + inserted_customer["description"] = Faker().sentence() + + updated_customer = dao.update(inserted_customer["id"], domain.Customer(**inserted_customer)) + + assert isinstance(updated_customer, domain.Customer) + assert updated_customer.description == inserted_customer["description"] + assert updated_customer.__dict__ == inserted_customer + + +def test__update_customer_dao__returns_none__when_an_incorrect_id_is_passed( + test_db, customer_factory, create_fake_dao, insert_customer +): + dao = create_fake_dao(test_db) + existent_customer = customer_factory() + + updated_customer = dao.update(Faker().pyint(), existent_customer) + + assert updated_customer is None diff --git a/V2/tests/unit/services/customer_service_test.py b/V2/tests/unit/services/customer_service_test.py index bb25070f..776d18ee 100644 --- a/V2/tests/unit/services/customer_service_test.py +++ b/V2/tests/unit/services/customer_service_test.py @@ -1,3 +1,5 @@ +from faker import Faker + from time_tracker.customers._domain import CustomerService @@ -12,3 +14,57 @@ def test__create_customer__uses_the_customer_dao__to_create_a_customer(mocker, c assert customer_dao.create.called assert expected_customer == new_customer + + +def test__delete_customer__uses_the_customer_dao__to_delete_customer_selected( + mocker, +): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + delete=mocker.Mock(return_value=expected_customer) + ) + + customer_service = CustomerService(customer_dao) + deleted_customer = customer_service.delete(Faker().pyint()) + + assert customer_dao.delete.called + assert expected_customer == deleted_customer + + +def test__get_all__uses_the_customer_dao__to_retrieve_customers(mocker): + expected_customers = mocker.Mock() + customer_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_customers) + ) + customer_service = CustomerService(customer_dao) + + actual_customers = customer_service.get_all() + + assert customer_dao.get_all.called + assert expected_customers == actual_customers + + +def test__get_by_id__uses_the_customer_dao__to_retrieve_one_customer(mocker): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_customer) + ) + customer_service = CustomerService(customer_dao) + + actual_customer = customer_service.get_by_id(Faker().pyint()) + + assert customer_dao.get_by_id.called + assert expected_customer == actual_customer + + +def test__update_customer__uses_the_customer_dao__to_update_a_customer(mocker, customer_factory): + expected_customer = mocker.Mock() + customer_dao = mocker.Mock( + update=mocker.Mock(return_value=expected_customer) + ) + customer_service = CustomerService(customer_dao) + + updated_customer = customer_service.update(Faker().pyint(), customer_factory()) + + assert customer_dao.update.called + assert expected_customer == updated_customer diff --git a/V2/tests/unit/use_cases/customers_use_case_test.py b/V2/tests/unit/use_cases/customers_use_case_test.py index 3b8566a9..63e03081 100644 --- a/V2/tests/unit/use_cases/customers_use_case_test.py +++ b/V2/tests/unit/use_cases/customers_use_case_test.py @@ -1,4 +1,5 @@ from pytest_mock import MockFixture +from faker import Faker from time_tracker.customers._domain import _use_cases @@ -16,3 +17,61 @@ def test__create_customer_function__uses_the_customer_service__to_create_a_custo assert customer_service.create.called assert expected_customer == new_customer + + +def test__delete_customer_function__uses_the_customer_service__to_delete_customer_selected( + mocker: MockFixture, +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock(delete=mocker.Mock(return_value=expected_customer)) + + customer_use_case = _use_cases.DeleteCustomerUseCase(customer_service) + deleted_customer = customer_use_case.delete_customer(Faker().pyint()) + + assert customer_service.delete.called + assert expected_customer == deleted_customer + + +def test__get_list_customers_function__uses_the_customer_service__to_retrieve_customers( + mocker: MockFixture, +): + expected_customers = mocker.Mock() + customer_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_customers) + ) + + customers_use_case = _use_cases.GetAllCustomerUseCase(customer_service) + actual_customers = customers_use_case.get_all_customer() + + assert customer_service.get_all.called + assert expected_customers == actual_customers + + +def test__get_customer_by_id_function__uses_the_customer_service__to_retrieve_customer( + mocker: MockFixture, +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_customer) + ) + + customer_use_case = _use_cases.GetByIdCustomerUseCase(customer_service) + actual_customer = customer_use_case.get_customer_by_id(Faker().pyint()) + + assert customer_service.get_by_id.called + assert expected_customer == actual_customer + + +def test__update_customer_function__uses_the_customer_service__to_update_a_customer( + mocker: MockFixture, customer_factory +): + expected_customer = mocker.Mock() + customer_service = mocker.Mock( + update=mocker.Mock(return_value=expected_customer) + ) + + customer_use_case = _use_cases.UpdateCustomerUseCase(customer_service) + updated_customer = customer_use_case.update_customer(Faker().pyint(), customer_factory()) + + assert customer_service.update.called + assert expected_customer == updated_customer diff --git a/V2/time_tracker/customers/_application/__init__.py b/V2/time_tracker/customers/_application/__init__.py index db2c2c15..d9ba1676 100644 --- a/V2/time_tracker/customers/_application/__init__.py +++ b/V2/time_tracker/customers/_application/__init__.py @@ -1,2 +1,5 @@ # flake8: noqa -from ._customers import create_customer \ No newline at end of file +from ._customers import create_customer +from ._customers import get_customers +from ._customers import delete_customer +from ._customers import update_customer diff --git a/V2/time_tracker/customers/_application/_customers/__init__.py b/V2/time_tracker/customers/_application/_customers/__init__.py index bf1f8460..b07840ce 100644 --- a/V2/time_tracker/customers/_application/_customers/__init__.py +++ b/V2/time_tracker/customers/_application/_customers/__init__.py @@ -1,2 +1,5 @@ # flake8: noqa -from ._create_customer import create_customer \ No newline at end of file +from ._create_customer import create_customer +from ._get_customers import get_customers +from ._delete_customer import delete_customer +from ._update_customer import update_customer diff --git a/V2/time_tracker/customers/_application/_customers/_create_customer.py b/V2/time_tracker/customers/_application/_customers/_create_customer.py index 919c34cb..48e39dc3 100644 --- a/V2/time_tracker/customers/_application/_customers/_create_customer.py +++ b/V2/time_tracker/customers/_application/_customers/_create_customer.py @@ -1,6 +1,7 @@ import dataclasses import json import typing +from http import HTTPStatus import azure.functions as func @@ -17,8 +18,8 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: use_case = _domain._use_cases.CreateCustomerUseCase(customer_service) customer_data = req.get_json() - customer_is_valid = _validate_customer(customer_data) - if not customer_is_valid: + customer_is_invalid = _validate_customer(customer_data) + if customer_is_invalid: raise ValueError customer_to_create = _domain.Customer( @@ -32,10 +33,10 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: if created_customer: body = json.dumps(created_customer.__dict__) - status_code = 201 + status_code = HTTPStatus.CREATED else: body = b'This customer already exists' - status_code = 409 + status_code = HTTPStatus.CONFLICT return func.HttpResponse( body=body, @@ -45,13 +46,11 @@ def create_customer(req: func.HttpRequest) -> func.HttpResponse: except ValueError: return func.HttpResponse( body=b'Invalid format or structure of the attributes of the customer', - status_code=400, + status_code=HTTPStatus.BAD_REQUEST, mimetype="application/json" ) -def _validate_customer(customer_data: dict) -> bool: - if [field.name for field in dataclasses.fields(_domain.Customer) - if (field.name not in customer_data) and (field.type != typing.Optional[field.type])]: - return False - return True +def _validate_customer(customer_data: dict) -> typing.List[str]: + return [field.name for field in dataclasses.fields(_domain.Customer) + if (field.name not in customer_data) and (field.type != typing.Optional[field.type])] diff --git a/V2/time_tracker/customers/_application/_customers/_delete_customer.py b/V2/time_tracker/customers/_application/_customers/_delete_customer.py new file mode 100644 index 00000000..41fc3464 --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_delete_customer.py @@ -0,0 +1,39 @@ +import json +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +DATATYPE = "application/json" + + +def delete_customer(req: func.HttpRequest) -> func.HttpResponse: + customer_dao = _infrastructure.CustomersSQLDao(DB()) + customer_service = _domain.CustomerService(customer_dao) + use_case = _domain._use_cases.DeleteCustomerUseCase(customer_service) + + try: + customer_id = int(req.route_params.get("id")) + deleted_customer = use_case.delete_customer(customer_id) + if not deleted_customer: + return func.HttpResponse( + body="Not found", + status_code=HTTPStatus.NOT_FOUND, + mimetype=DATATYPE + ) + + return func.HttpResponse( + body=json.dumps(deleted_customer.__dict__, default=str), + status_code=HTTPStatus.OK, + mimetype=DATATYPE, + ) + + except ValueError: + return func.HttpResponse( + body=b"Invalid Format ID", + status_code=HTTPStatus.BAD_REQUEST, + mimetype=DATATYPE + ) diff --git a/V2/time_tracker/customers/_application/_customers/_get_customers.py b/V2/time_tracker/customers/_application/_customers/_get_customers.py new file mode 100644 index 00000000..8cb9635f --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_get_customers.py @@ -0,0 +1,55 @@ +from http import HTTPStatus +import json + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def get_customers(req: func.HttpRequest) -> func.HttpResponse: + customer_id = req.route_params.get('id') + status_code = HTTPStatus.OK + + try: + if customer_id: + response = _get_by_id(int(customer_id)) + if response == b'This customer does not exist': + status_code = HTTPStatus.NOT_FOUND + else: + response = _get_all() + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b"The id has an invalid format", status_code=HTTPStatus.BAD_REQUEST, mimetype="application/json" + ) + + +def _get_by_id(customer_id: int) -> str: + customer_use_case = _domain._use_cases.GetByIdCustomerUseCase( + _create_customer_service(DB()) + ) + customer = customer_use_case.get_customer_by_id(customer_id) + + return json.dumps(customer.__dict__) if customer else b'This customer does not exist' + + +def _get_all() -> str: + customer_sql = _domain._use_cases.GetAllCustomerUseCase( + _create_customer_service(DB()) + ) + return json.dumps( + [ + customer.__dict__ + for customer in customer_sql.get_all_customer() + ] + ) + + +def _create_customer_service(db: DB) -> _domain.CustomerService: + customer_sql = _infrastructure.CustomersSQLDao(db) + return _domain.CustomerService(customer_sql) diff --git a/V2/time_tracker/customers/_application/_customers/_update_customer.py b/V2/time_tracker/customers/_application/_customers/_update_customer.py new file mode 100644 index 00000000..93524c65 --- /dev/null +++ b/V2/time_tracker/customers/_application/_customers/_update_customer.py @@ -0,0 +1,53 @@ +import dataclasses +import json +import typing +from http import HTTPStatus + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + + +def update_customer(req: func.HttpRequest) -> func.HttpResponse: + try: + database = DB() + customer_id = int(req.route_params.get('id')) + customer_dao = _infrastructure.CustomersSQLDao(database) + customer_service = _domain.CustomerService(customer_dao) + use_case = _domain._use_cases.UpdateCustomerUseCase(customer_service) + + customer_data = req.get_json() + customer_is_invalid = _validate_customer(customer_data) + if customer_is_invalid: + raise ValueError + + customer_to_update = _domain.Customer( + **{field.name: customer_data.get(field.name) for field in dataclasses.fields(_domain.Customer)} + ) + updated_customer = use_case.update_customer(customer_id, customer_to_update) + + if updated_customer: + body = json.dumps(updated_customer.__dict__) + status_code = HTTPStatus.OK + else: + body = b'This customer does not exist or is duplicated' + status_code = HTTPStatus.CONFLICT + + return func.HttpResponse( + body=body, + status_code=status_code, + mimetype="application/json" + ) + except ValueError: + return func.HttpResponse( + body=b'Invalid format or structure of the attributes of the customer', + status_code=HTTPStatus.BAD_REQUEST, + mimetype="application/json" + ) + + +def _validate_customer(customer_data: dict) -> typing.List[str]: + return [field.name for field in dataclasses.fields(_domain.Customer) + if field.name not in customer_data] diff --git a/V2/time_tracker/customers/_domain/__init__.py b/V2/time_tracker/customers/_domain/__init__.py index 8392b8e9..a2e8014b 100644 --- a/V2/time_tracker/customers/_domain/__init__.py +++ b/V2/time_tracker/customers/_domain/__init__.py @@ -4,4 +4,8 @@ from ._services import CustomerService from ._use_cases import ( CreateCustomerUseCase, + UpdateCustomerUseCase, + GetAllCustomerUseCase, + GetByIdCustomerUseCase, + DeleteCustomerUseCase ) \ No newline at end of file diff --git a/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py b/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py index 35a7a7e9..186d5c86 100644 --- a/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py +++ b/V2/time_tracker/customers/_domain/_persistence_contracts/_customers_dao.py @@ -1,4 +1,5 @@ import abc +import typing from time_tracker.customers._domain import Customer @@ -7,3 +8,19 @@ class CustomersDao(abc.ABC): @abc.abstractmethod def create(self, data: Customer) -> Customer: pass + + @abc.abstractmethod + def update(self, id: int, data: Customer) -> Customer: + pass + + @abc.abstractmethod + def get_by_id(self, id: int) -> Customer: + pass + + @abc.abstractmethod + def get_all(self) -> typing.List[Customer]: + pass + + @abc.abstractmethod + def delete(self, id: int) -> Customer: + pass diff --git a/V2/time_tracker/customers/_domain/_services/_customer.py b/V2/time_tracker/customers/_domain/_services/_customer.py index 88633a08..082a7b08 100644 --- a/V2/time_tracker/customers/_domain/_services/_customer.py +++ b/V2/time_tracker/customers/_domain/_services/_customer.py @@ -1,3 +1,5 @@ +import typing + from time_tracker.customers._domain import Customer, CustomersDao @@ -8,3 +10,15 @@ def __init__(self, customer_dao: CustomersDao): def create(self, data: Customer) -> Customer: return self.customer_dao.create(data) + + def update(self, id: int, data: Customer) -> Customer: + return self.customer_dao.update(id, data) + + def get_by_id(self, id: int) -> Customer: + return self.customer_dao.get_by_id(id) + + def get_all(self) -> typing.List[Customer]: + return self.customer_dao.get_all() + + def delete(self, id: int) -> Customer: + return self.customer_dao.delete(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/__init__.py b/V2/time_tracker/customers/_domain/_use_cases/__init__.py index accd4281..4dcb8239 100644 --- a/V2/time_tracker/customers/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/customers/_domain/_use_cases/__init__.py @@ -1,2 +1,6 @@ # flake8: noqa -from ._create_customer_use_case import CreateCustomerUseCase \ No newline at end of file +from ._create_customer_use_case import CreateCustomerUseCase +from ._update_customer_use_case import UpdateCustomerUseCase +from ._get_by_id_customer_use_case import GetByIdCustomerUseCase +from ._get_all_customer_use_case import GetAllCustomerUseCase +from ._delete_customer_use_case import DeleteCustomerUseCase diff --git a/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py new file mode 100644 index 00000000..0477a1f2 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_delete_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class DeleteCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def delete_customer(self, id: int) -> Customer: + return self.customer_service.delete(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py new file mode 100644 index 00000000..d3780449 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_get_all_customer_use_case.py @@ -0,0 +1,12 @@ +import typing + +from time_tracker.customers._domain import Customer, CustomerService + + +class GetAllCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def get_all_customer(self) -> typing.List[Customer]: + return self.customer_service.get_all() diff --git a/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py new file mode 100644 index 00000000..2372029a --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_get_by_id_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class GetByIdCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def get_customer_by_id(self, id: int) -> Customer: + return self.customer_service.get_by_id(id) diff --git a/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py b/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py new file mode 100644 index 00000000..318ced28 --- /dev/null +++ b/V2/time_tracker/customers/_domain/_use_cases/_update_customer_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.customers._domain import Customer, CustomerService + + +class UpdateCustomerUseCase: + + def __init__(self, customer_service: CustomerService): + self.customer_service = customer_service + + def update_customer(self, id: int, data: Customer) -> Customer: + return self.customer_service.update(id, data) diff --git a/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py b/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py index 2b1f4c0d..f3b15122 100644 --- a/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py +++ b/V2/time_tracker/customers/_infrastructure/_data_persistence/_customer_dao.py @@ -1,4 +1,5 @@ import dataclasses +import typing import sqlalchemy as sq @@ -22,6 +23,21 @@ def __init__(self, database: _db.DB): extend_existing=True, ) + def get_by_id(self, id: int) -> domain.Customer: + query = sq.sql.select(self.customer).where( + sq.sql.and_(self.customer.c.id == id, self.customer.c.deleted.is_(False)) + ) + customer = self.db.get_session().execute(query).one_or_none() + return self.__create_customer_dto(dict(customer)) if customer else None + + def get_all(self) -> typing.List[domain.Customer]: + query = sq.sql.select(self.customer).where(self.customer.c.deleted.is_(False)) + result = self.db.get_session().execute(query) + return [ + self.__create_customer_dto(dict(customer)) + for customer in result + ] + def create(self, data: domain.Customer) -> domain.Customer: try: new_customer = data.__dict__ @@ -39,3 +55,26 @@ def create(self, data: domain.Customer) -> domain.Customer: def __create_customer_dto(self, customer: dict) -> domain.Customer: customer = {key: customer.get(key) for key in self.customer_key} return domain.Customer(**customer) + + def delete(self, customer_id: int) -> domain.Customer: + query = ( + self.customer.update() + .where(self.customer.c.id == customer_id) + .values({"deleted": True}) + ) + self.db.get_session().execute(query) + query_deleted_customer = sq.sql.select(self.customer).where(self.customer.c.id == customer_id) + customer = self.db.get_session().execute(query_deleted_customer).one_or_none() + return self.__create_customer_dto(dict(customer)) if customer else None + + def update(self, id: int, data: domain.Customer) -> domain.Customer: + try: + new_customer = data.__dict__ + new_customer.pop("id") + + customer_validated = {key: value for (key, value) in new_customer.items() if value is not None} + query = self.customer.update().where(self.customer.c.id == id).values(customer_validated) + self.db.get_session().execute(query) + return self.get_by_id(id) + except sq.exc.SQLAlchemyError: + return None diff --git a/V2/time_tracker/customers/interface.py b/V2/time_tracker/customers/interface.py index e36b8172..9aef2091 100644 --- a/V2/time_tracker/customers/interface.py +++ b/V2/time_tracker/customers/interface.py @@ -1,2 +1,5 @@ # flake8: noqa from ._application import create_customer +from ._application import get_customers +from ._application import delete_customer +from ._application import update_customer