diff --git a/backend/alembic/versions/2024_10_01_2048-acfeadc7868c_adding_events_for_can_funding_received.py b/backend/alembic/versions/2024_10_01_2048-acfeadc7868c_adding_events_for_can_funding_received.py new file mode 100644 index 0000000000..7215d69446 --- /dev/null +++ b/backend/alembic/versions/2024_10_01_2048-acfeadc7868c_adding_events_for_can_funding_received.py @@ -0,0 +1,34 @@ +"""adding events for can funding received + +Revision ID: acfeadc7868c +Revises: d5610e0988b0 +Create Date: 2024-10-01 20:48:34.638703+00:00 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from alembic_postgresql_enum import TableReference + +# revision identifiers, used by Alembic. +revision: str = 'acfeadc7868c' +down_revision: Union[str, None] = 'd5610e0988b0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values('ops', 'opseventtype', ['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'CREATE_NEW_CAN', 'UPDATE_CAN', 'DELETE_CAN', 'CREATE_CAN_FUNDING_RECEIVED', 'UPDATE_CAN_FUNDING_RECEIVED', 'DELETE_CAN_FUNDING_RECEIVED', 'CREATE_CAN_FUNDING_BUDGET', 'UPDATE_CAN_FUNDING_BUDGET', 'DELETE_CAN_FUNDING_BUDGET', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'], + [TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')], + enum_values_to_rename=[]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values('ops', 'opseventtype', ['CREATE_BLI', 'UPDATE_BLI', 'DELETE_BLI', 'SEND_BLI_FOR_APPROVAL', 'CREATE_PROJECT', 'CREATE_NEW_AGREEMENT', 'UPDATE_AGREEMENT', 'DELETE_AGREEMENT', 'CREATE_NEW_CAN', 'UPDATE_CAN', 'DELETE_CAN', 'CREATE_CAN_FUNDING_BUDGET', 'UPDATE_CAN_FUNDING_BUDGET', 'DELETE_CAN_FUNDING_BUDGET', 'ACKNOWLEDGE_NOTIFICATION', 'CREATE_BLI_PACKAGE', 'UPDATE_BLI_PACKAGE', 'CREATE_SERVICES_COMPONENT', 'UPDATE_SERVICES_COMPONENT', 'DELETE_SERVICES_COMPONENT', 'CREATE_PROCUREMENT_ACQUISITION_PLANNING', 'UPDATE_PROCUREMENT_ACQUISITION_PLANNING', 'DELETE_PROCUREMENT_ACQUISITION_PLANNING', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'LOGIN_ATTEMPT', 'LOGOUT', 'GET_USER_DETAILS', 'CREATE_USER', 'UPDATE_USER', 'DEACTIVATE_USER'], + [TableReference(table_schema='ops', table_name='ops_event', column_name='event_type'), TableReference(table_schema='ops', table_name='ops_event_version', column_name='event_type')], + enum_values_to_rename=[]) + # ### end Alembic commands ### diff --git a/backend/models/events.py b/backend/models/events.py index d283225bce..2c4e322ed4 100644 --- a/backend/models/events.py +++ b/backend/models/events.py @@ -27,6 +27,11 @@ class OpsEventType(Enum): UPDATE_CAN = auto() DELETE_CAN = auto() + # CAN Funding Received Related Events + CREATE_CAN_FUNDING_RECEIVED = auto() + UPDATE_CAN_FUNDING_RECEIVED = auto() + DELETE_CAN_FUNDING_RECEIVED = auto() + # CAN Funding Budget Related Events CREATE_CAN_FUNDING_BUDGET = auto() UPDATE_CAN_FUNDING_BUDGET = auto() diff --git a/backend/openapi.yml b/backend/openapi.yml index 56910e8256..8a787d2b37 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -370,6 +370,174 @@ paths: description: Bad Request "401": description: Unauthorized + /api/v1/cans-funding-received/: + get: + tags: + - CANs Funding Received + operationId: getAllCANFundingReceived + summary: Get a list all CANs funding received + parameters: + - $ref: "#/components/parameters/simulatedError" + description: Get CANFundingReceived + responses: + "200": + description: OK + post: + tags: + - CANs Funding Received + operationId: createCANFundingReceived + summary: Create a new CANFundingReceived object + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUpdateFundingReceived" + examples: + "0": + $ref: "#/components/examples/CreateUpdateFundingReceived" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/FundingReceived" + "400": + description: Bad Request + "401": + description: Insufficient Privileges to use this endpoint. + /api/v1/can-funding-received/{can_funding_received_id}: + get: + tags: + - CAN Funding Received + summary: Get an individual CANFundingReceived + operationId: getCANFundingReceivedById + description: Get CANFundingReceived by Id + parameters: + - $ref: "#/components/parameters/simulatedError" + - in: path + name: can_funding_received_id + description: CAN Funding Received Id + required: true + schema: + type: integer + format: int32 + minimum: 0 + default: 1 + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/FundingReceived" + example: + "0": + $ref: "#/components/examples/FundingReceived" + "404": + description: The specified CANFundingReceived was not found. + patch: + tags: + - CANs Funding Received + summary: Update an existing CANFundingReceived with the provided fields + operationId: patchCANFundingReceived + parameters: + - $ref: "#/components/parameters/simulatedError" + - in: path + name: can_funding_received_id + description: CAN Funding Received Id + required: true + schema: + type: integer + format: int32 + minimum: 0 + default: 1 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUpdateFundingReceived" + examples: + "0": + $ref: "#/components/examples/CreateUpdateFundingReceived" + responses: + "200": + description: CANFundingReceivedUpdated + content: + application/json: + schema: + $ref: "#/components/schemas/FundingReceived" + "400": + description: Bad Request + "401": + description: Insufficient Privileges to use this endpoint. + "404": + description: CANFundingReceived not found + put: + tags: + - CAN Funding Received + summary: Update an existing CANFundingReceived + operationId: putCanFundingReceived + parameters: + - $ref: "#/components/parameters/simulatedError" + - in: path + name: can_funding_received_id + description: CAN Funding Received Id + required: true + schema: + type: integer + format: int32 + minimum: 0 + default: 1 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUpdateFundingReceived" + examples: + "0": + $ref: "#/components/examples/CreateUpdateFundingReceived" + responses: + "200": + description: CAN Updated + content: + application/json: + schema: + $ref: "#/components/schemas/FundingReceived" + "400": + description: Bad Request + "401": + description: Insufficient Privileges to use this endpoint. + "404": + description: CAN not found + delete: + tags: + - CAN Funding Received + operationId: deleteCanFundingReceived + summary: Delete a CANFundingReceived + parameters: + - $ref: "#/components/parameters/simulatedError" + - in: path + name: can_funding_received_id + description: CANFundingReceived ID + required: true + schema: + type: integer + format: int32 + minimum: 1 + default: 1 + responses: + "200": + description: OK + "400": + description: Bad Request + "401": + description: Unauthorized + "404": + description: Unable to find specified CANFundingReceived /api/v1/can-funding-budgets/: get: tags: @@ -2439,6 +2607,21 @@ components: - id - can_id - fiscal_year + CreateUpdateFundingReceived: + description: A request object for the creation or updating of a FundingReceived + type: object + properties: + fiscal_year: + type: integer + can_id: + type: integer + funding: + type: number + notes: + type: string + required: + - fiscal_year + - can_id FundingDetails: description: Funding Detail Object type: object @@ -3940,14 +4123,33 @@ components: portfolio_id: 6, description: "A very good CAN to use for examples." } - CreateUpdateCANFundingBudgetRequest: + CreateUpdateFundingReceived: value: | { can_id: 500, fiscal_year: 2024, - budget: 123456, + funding: 123456, notes: "A new test Funding Budget" } + FundingReceived: + value: | + { + funding: 1234567.89 + can: {} + can_id: 500 + display_name: CANFundingBudget.1 + fiscal_year: 2024 + id: 1 + notes: "A funding budget" + versions: {} + CreateUpdateCANFundingBudgetRequest: + value: | + { + can_id: 500, + fiscal_year: 2024, + funding: 123456, + notes: "A new test Funding Funding" + } FundingBudget: value: | { diff --git a/backend/ops_api/ops/resources/can_funding_received.py b/backend/ops_api/ops/resources/can_funding_received.py new file mode 100644 index 0000000000..b92226a7ce --- /dev/null +++ b/backend/ops_api/ops/resources/can_funding_received.py @@ -0,0 +1,95 @@ +from flask import Response, request +from flask_jwt_extended import jwt_required + +from models import OpsEventType +from ops_api.ops.auth.auth_types import Permission, PermissionType +from ops_api.ops.auth.decorators import is_authorized +from ops_api.ops.base_views import BaseItemAPI, BaseListAPI +from ops_api.ops.schemas.cans import CreateUpdateFundingReceivedSchema, FundingReceivedSchema +from ops_api.ops.services.can_funding_received import CANFundingReceivedService +from ops_api.ops.utils.errors import error_simulator +from ops_api.ops.utils.events import OpsEventHandler +from ops_api.ops.utils.response import make_response_with_headers + + +class CANFundingReceivedItemAPI(BaseItemAPI): + def __init__(self, model): + super().__init__(model) + self.can_service = CANFundingReceivedService() + + @is_authorized(PermissionType.GET, Permission.CAN) + def get(self, id: int) -> Response: + schema = FundingReceivedSchema() + item = self.can_service.get(id) + return make_response_with_headers(schema.dump(item)) + + @is_authorized(PermissionType.PATCH, Permission.CAN) + def patch(self, id: int) -> Response: + """ + Update a CANFundingReceived with only the fields provided in the request body. + """ + with OpsEventHandler(OpsEventType.UPDATE_CAN_FUNDING_RECEIVED) as meta: + request_data = request.get_json() + # Setting partial to true ignores any missing fields. + schema = CreateUpdateFundingReceivedSchema(partial=True) + serialized_request = schema.load(request_data) + + updated_funding_received = self.can_service.update(serialized_request, id) + serialized_can_funding_received = schema.dump(updated_funding_received) + meta.metadata.update({"updated_funding_received": serialized_can_funding_received}) + return make_response_with_headers(serialized_can_funding_received) + + @is_authorized(PermissionType.PATCH, Permission.CAN) + def put(self, id: int) -> Response: + """ + Update a CANFundingReceived + """ + with OpsEventHandler(OpsEventType.UPDATE_CAN_FUNDING_RECEIVED) as meta: + request_data = request.get_json() + schema = CreateUpdateFundingReceivedSchema() + serialized_request = schema.load(request_data) + + updated_funding_received = self.can_service.update(serialized_request, id) + serialized_funding_received = schema.dump(updated_funding_received) + meta.metadata.update({"updated_can_funding_budget": serialized_funding_received}) + return make_response_with_headers(serialized_funding_received) + + @is_authorized(PermissionType.DELETE, Permission.CAN) + def delete(self, id: int) -> Response: + """ + Delete a CANFundingReceived with given id. + """ + with OpsEventHandler(OpsEventType.DELETE_CAN_FUNDING_RECEIVED) as meta: + self.can_service.delete(id) + meta.metadata.update({"Deleted CANFundingReceived": id}) + return make_response_with_headers({"message": "CANFundingReceived deleted", "id": id}, 200) + + +class CANFundingReceivedListAPI(BaseListAPI): + def __init__(self, model): + super().__init__(model) + self.can_service = CANFundingReceivedService() + + @jwt_required() + @error_simulator + def get(self) -> Response: + result = self.can_service.get_list() + funding_received_schema = FundingReceivedSchema() + return make_response_with_headers([funding_received_schema.dump(can) for can in result]) + + @is_authorized(PermissionType.POST, Permission.CAN) + def post(self) -> Response: + """ + Create a new CANFundingReceived object + """ + with OpsEventHandler(OpsEventType.CREATE_CAN_FUNDING_RECEIVED) as meta: + request_data = request.get_json() + schema = CreateUpdateFundingReceivedSchema() + serialized_request = schema.load(request_data) + + created_funding_received = self.can_service.create(serialized_request) + + funding_received_schema = FundingReceivedSchema() + serialized_funding_received = funding_received_schema.dump(created_funding_received) + meta.metadata.update({"new_can_funding_received": serialized_funding_received}) + return make_response_with_headers(serialized_funding_received, 201) diff --git a/backend/ops_api/ops/schemas/cans.py b/backend/ops_api/ops/schemas/cans.py index d832a085f5..befc0b5e9d 100644 --- a/backend/ops_api/ops/schemas/cans.py +++ b/backend/ops_api/ops/schemas/cans.py @@ -136,6 +136,13 @@ class FundingReceivedSchema(Schema): updated_by_user = fields.Nested(SafeUserSchema(), allow_none=True) +class CreateUpdateFundingReceivedSchema(Schema): + fiscal_year = fields.Integer(required=True) + can_id = fields.Integer(required=True) + funding = fields.Integer(load_default=None) + notes = fields.String(load_default=None) + + class CANSchema(BasicCANSchema): budget_line_items = fields.List(fields.Nested(BudgetLineItemResponseSchema()), default=[]) funding_budgets = fields.List(fields.Nested(FundingBudgetSchema()), default=[]) diff --git a/backend/ops_api/ops/services/can_funding_received.py b/backend/ops_api/ops/services/can_funding_received.py new file mode 100644 index 0000000000..a21f09e891 --- /dev/null +++ b/backend/ops_api/ops/services/can_funding_received.py @@ -0,0 +1,87 @@ +from flask import current_app +from sqlalchemy import select +from sqlalchemy.exc import NoResultFound +from werkzeug.exceptions import NotFound + +from models import CANFundingReceived + + +class CANFundingReceivedService: + def _update_fields(self, old_funding_received: CANFundingReceived, funding_update) -> bool: + """ + Update fields on the CAN based on the fields passed in can_update. + Returns true if any fields were updated. + """ + is_changed = False + for attr, value in funding_update.items(): + if getattr(old_funding_received, attr) != value: + setattr(old_funding_received, attr, value) + is_changed = True + + return is_changed + + def create(self, create_funding_received_request) -> CANFundingReceived: + """ + Create a new CANFundingReceived object and save it to the database + """ + new_can = CANFundingReceived(**create_funding_received_request) + + current_app.db_session.add(new_can) + current_app.db_session.commit() + return new_can + + def update(self, updated_fields, id: int) -> CANFundingReceived: + """ + Update a CANFundingReceived object with only the provided values in updated_fields. + """ + try: + old_funding_received: CANFundingReceived = current_app.db_session.execute( + select(CANFundingReceived).where(CANFundingReceived.id == id) + ).scalar_one() + + funding_received_was_updated = self._update_fields(old_funding_received, updated_fields) + if funding_received_was_updated: + current_app.db_session.add(old_funding_received) + current_app.db_session.commit() + + return old_funding_received + except NoResultFound: + current_app.logger.exception(f"Could not find a CANFundingReceived with id {id}") + raise NotFound() + + def delete(self, id: int): + """ + Delete a CANFundingReceived with given id. Throw a NotFound error if no CAN corresponding to that ID exists.""" + try: + old_funding = current_app.db_session.get(CANFundingReceived, id) + + if old_funding is None: + raise NotFound(f"No CANFundingReceived found with id {id}") + + current_app.db_session.delete(old_funding) + current_app.db_session.commit() + + except NotFound as e: + current_app.logger.exception(f"Could not find a CANFundingReceived with id {id}") + raise e + + def get(self, id: int) -> CANFundingReceived: + """ + Get an individual CANFundingReceived object by id. + """ + stmt = select(CANFundingReceived).where(CANFundingReceived.id == id) + can_funding_received = current_app.db_session.scalar(stmt) + + if can_funding_received: + return can_funding_received + else: + current_app.logger.exception(f"Could not find a CAN with id {id}") + raise NotFound() + + def get_list(self) -> list[CANFundingReceived]: + """ + Get a list of CANFundingReceived objects. + """ + stmt = select(CANFundingReceived).order_by(CANFundingReceived.id) + results = current_app.db_session.execute(stmt).all() + return [can for result in results for can in result] diff --git a/backend/ops_api/ops/urls.py b/backend/ops_api/ops/urls.py index 4bcbe855c9..f0e42b93cd 100644 --- a/backend/ops_api/ops/urls.py +++ b/backend/ops_api/ops/urls.py @@ -11,6 +11,8 @@ AZURE_SAS_TOKEN_VIEW_FUNC, BUDGET_LINE_ITEMS_ITEM_API_VIEW_FUNC, BUDGET_LINE_ITEMS_LIST_API_VIEW_FUNC, + CAN_FUNDING_RECEIVED_ITEM_API_VIEW_FUNC, + CAN_FUNDING_RECEIVED_LIST_API_VIEW_FUNC, CAN_FUNDING_BUDGET_ITEM_API_VIEW_FUNC, CAN_FUNDING_BUDGET_LIST_API_VIEW_FUNC, CAN_FUNDING_SUMMARY_ITEM_API_VIEW_FUNC, @@ -101,6 +103,14 @@ def register_api(api_bp: Blueprint) -> None: api_bp.add_url_rule("/can-funding-budgets/", view_func=CAN_FUNDING_BUDGET_LIST_API_VIEW_FUNC) + api_bp.add_url_rule( + "/cans-funding-received/", + view_func=CAN_FUNDING_RECEIVED_LIST_API_VIEW_FUNC, + ) + api_bp.add_url_rule( + "/cans-funding-received/", + view_func=CAN_FUNDING_RECEIVED_ITEM_API_VIEW_FUNC, + ) api_bp.add_url_rule( "/ops-db-histories/", view_func=OPS_DB_HISTORY_LIST_API_VIEW_FUNC, diff --git a/backend/ops_api/ops/views.py b/backend/ops_api/ops/views.py index 60425c1e61..a63c326080 100644 --- a/backend/ops_api/ops/views.py +++ b/backend/ops_api/ops/views.py @@ -8,6 +8,7 @@ BudgetLineItem, CANFundingBudget, CANFundingDetails, + CANFundingReceived, ChangeRequest, ContractAgreement, Division, @@ -43,6 +44,7 @@ ) from ops_api.ops.resources.azure import SasToken from ops_api.ops.resources.budget_line_items import BudgetLineItemsItemAPI, BudgetLineItemsListAPI +from ops_api.ops.resources.can_funding_received import CANFundingReceivedItemAPI, CANFundingReceivedListAPI from ops_api.ops.resources.can_funding_budget import CANFundingBudgetItemAPI, CANFundingBudgetListAPI from ops_api.ops.resources.can_funding_summary import CANFundingSummaryItemAPI from ops_api.ops.resources.cans import CANItemAPI, CANListAPI, CANsByPortfolioAPI @@ -101,6 +103,12 @@ CAN_ITEM_API_VIEW_FUNC = CANItemAPI.as_view("can-item", CAN) CAN_LIST_API_VIEW_FUNC = CANListAPI.as_view("can-group", CAN) CANS_BY_PORTFOLIO_API_VIEW_FUNC = CANsByPortfolioAPI.as_view("can-portfolio", BaseModel) +CAN_FUNDING_RECEIVED_LIST_API_VIEW_FUNC = CANFundingReceivedListAPI.as_view( + "can-funding-received-group", CANFundingReceived +) +CAN_FUNDING_RECEIVED_ITEM_API_VIEW_FUNC = CANFundingReceivedItemAPI.as_view( + "can-funding-received-item", CANFundingReceived +) # BUDGET LINE ITEM ENDPOINTS BUDGET_LINE_ITEMS_ITEM_API_VIEW_FUNC = BudgetLineItemsItemAPI.as_view("budget-line-items-item", BudgetLineItem) diff --git a/backend/ops_api/tests/ops/can/test_can_funding_received.py b/backend/ops_api/tests/ops/can/test_can_funding_received.py new file mode 100644 index 0000000000..95b03c658d --- /dev/null +++ b/backend/ops_api/tests/ops/can/test_can_funding_received.py @@ -0,0 +1,285 @@ +import pytest +from sqlalchemy import select + +from models import CANFundingReceived +from ops.services.can_funding_received import CANFundingReceivedService + + +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_get_all(auth_client, mocker, test_can): + mocker_get_can = mocker.patch("ops_api.ops.services.can_funding_received.CANFundingReceivedService.get_list") + mocker_get_can.return_value = [test_can] + response = auth_client.get("/api/v1/cans-funding-received/") + assert response.status_code == 200 + assert len(response.json) == 1 + mocker_get_can.assert_called_once() + + +def test_funding_received_service_get_all(auth_client, loaded_db): + count = loaded_db.query(CANFundingReceived).count() + can_funding_received_service = CANFundingReceivedService() + response = can_funding_received_service.get_list() + assert len(response) == count + + +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_get_by_id(auth_client, mocker, test_can): + mocker_get_can = mocker.patch("ops_api.ops.services.can_funding_received.CANFundingReceivedService.get") + mocker_get_can.return_value = test_can + response = auth_client.get(f"/api/v1/cans-funding-received/{test_can.id}") + assert response.status_code == 200 + # assert response.json["number"] == "G99HRF2" + + +def test_funding_received_service_get_by_id(test_can): + service = CANFundingReceivedService() + can = service.get(test_can.id) + assert test_can.id == can.id + # assert test_can.number == can.number + + +# Testing CANFundingReceived Creation +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_post_creates_funding_received(budget_team_auth_client, mocker, loaded_db): + input_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "This is a note"} + + mock_output_data = CANFundingReceived(can_id=500, fiscal_year=2024, funding=123456, notes="This is a note") + mocker_create_funding_received = mocker.patch( + "ops_api.ops.services.can_funding_received.CANFundingReceivedService.create" + ) + mocker_create_funding_received.return_value = mock_output_data + response = budget_team_auth_client.post("/api/v1/cans-funding-received/", json=input_data) + + assert response.status_code == 201 + mocker_create_funding_received.assert_called_once_with(input_data) + assert response.json["id"] == mock_output_data.id + assert response.json["can_id"] == mock_output_data.can_id + assert response.json["fiscal_year"] == mock_output_data.fiscal_year + assert response.json["funding"] == mock_output_data.funding + + +@pytest.mark.usefixtures("app_ctx") +def test_basic_user_cannot_post_funding_received(basic_user_auth_client): + input_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "This is a note"} + response = basic_user_auth_client.post("/api/v1/cans-funding-received/", json=input_data) + + assert response.status_code == 401 + + +def test_service_create_funding_received(loaded_db): + input_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "This is a note"} + + service = CANFundingReceivedService() + + new_funding = service.create(input_data) + + funding_received = loaded_db.execute( + select(CANFundingReceived).where(CANFundingReceived.id == new_funding.id) + ).scalar_one() + + assert funding_received is not None + assert funding_received.can_id == 500 + assert funding_received.notes == "This is a note" + assert funding_received.fiscal_year == 2024 + assert funding_received.id == new_funding.id + assert funding_received == new_funding + + loaded_db.delete(new_funding) + loaded_db.commit() + + +# Testing updating CANs by PATCH +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_patch(budget_team_auth_client, mocker): + test_funding_id = 600 + update_data = { + "notes": "Fake test update", + } + + funding_received = CANFundingReceived(can_id=500, fiscal_year=2024, funding=123456, notes="This is a note") + mocker_update_funding_received = mocker.patch( + "ops_api.ops.services.can_funding_received.CANFundingReceivedService.update" + ) + funding_received.notes = update_data["notes"] + mocker_update_funding_received.return_value = funding_received + response = budget_team_auth_client.patch(f"/api/v1/cans-funding-received/{test_funding_id}", json=update_data) + + assert response.status_code == 200 + mocker_update_funding_received.assert_called_once_with(update_data, test_funding_id) + assert response.json["funding"] == funding_received.funding + assert response.json["notes"] == funding_received.notes + + +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_patch_404(budget_team_auth_client): + test_funding_id = 600 + update_data = { + "notes": "Test CANFundingReceived Created by unit test", + } + + response = budget_team_auth_client.patch(f"/api/v1/cans-funding-received/{test_funding_id}", json=update_data) + + assert response.status_code == 404 + + +@pytest.mark.usefixtures("app_ctx") +def test_basic_user_cannot_patch_funding_received(basic_user_auth_client): + data = { + "notes": "An updated can description", + } + response = basic_user_auth_client.patch("/api/v1/cans-funding-received/517", json=data) + + assert response.status_code == 401 + + +def test_service_patch_funding_received(loaded_db): + update_data = { + "notes": "Test Test Test", + } + + input_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "This is a note"} + + funding_received_service = CANFundingReceivedService() + + new_funding_received = funding_received_service.create(input_data) + + updated_funding_received = funding_received_service.update(update_data, new_funding_received.id) + + funding_received = loaded_db.execute( + select(CANFundingReceived).where(CANFundingReceived.id == new_funding_received.id) + ).scalar_one() + + assert funding_received is not None + assert funding_received.funding == 123456 + assert updated_funding_received.funding == 123456 + assert funding_received.notes == "Test Test Test" + assert updated_funding_received.notes == "Test Test Test" + + loaded_db.delete(new_funding_received) + loaded_db.commit() + + +# Testing updating CANFundingReceived by PUT +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_put(budget_team_auth_client, mocker): + test_funding_received_id = 517 + update_data = { + "can_id": 500, + "fiscal_year": 2024, + "funding": 234567, + } + + funding_received = CANFundingReceived(can_id=500, fiscal_year=2024, funding=123456, notes="This is a note") + + mocker_update_funding_received = mocker.patch( + "ops_api.ops.services.can_funding_received.CANFundingReceivedService.update" + ) + funding_received.funding = update_data["funding"] + mocker_update_funding_received.return_value = funding_received + response = budget_team_auth_client.put( + f"/api/v1/cans-funding-received/{test_funding_received_id}", json=update_data + ) + + update_data["notes"] = None + assert response.status_code == 200 + mocker_update_funding_received.assert_called_once_with(update_data, test_funding_received_id) + assert response.json["funding"] == funding_received.funding + assert response.json["can_id"] == funding_received.can_id + + +@pytest.mark.usefixtures("app_ctx") +def test_basic_user_cannot_put_funding_received(basic_user_auth_client): + data = { + "notes": "An updated can description", + } + response = basic_user_auth_client.put("/api/v1/cans-funding-received/517", json=data) + + assert response.status_code == 401 + + +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_put_404(budget_team_auth_client): + test_funding_received_id = 600 + update_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "Test test test"} + + response = budget_team_auth_client.put( + f"/api/v1/cans-funding-received/{test_funding_received_id}", json=update_data + ) + + assert response.status_code == 404 + + +def test_service_update_funding_received_with_nones(loaded_db): + update_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": None} + + test_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "Test Notes"} + + funding_received_service = CANFundingReceivedService() + + new_funding_received = funding_received_service.create(test_data) + + updated_funding_received = funding_received_service.update(update_data, new_funding_received.id) + + funding_received = loaded_db.execute( + select(CANFundingReceived).where(CANFundingReceived.id == updated_funding_received.id) + ).scalar_one() + + assert funding_received is not None + assert funding_received.can_id == 500 + assert updated_funding_received.can_id == 500 + assert funding_received.notes is None + assert updated_funding_received.notes is None + assert funding_received.fiscal_year == 2024 + assert updated_funding_received.fiscal_year == 2024 + assert funding_received.funding == 123456 + assert updated_funding_received.funding == 123456 + + loaded_db.delete(new_funding_received) + loaded_db.commit() + + +# Testing deleting CANFundingReceived +@pytest.mark.usefixtures("app_ctx") +def test_funding_received_delete(budget_team_auth_client, mocker): + test_funding_received_id = 517 + + mocker_delete_funding_received = mocker.patch( + "ops_api.ops.services.can_funding_received.CANFundingReceivedService.delete" + ) + response = budget_team_auth_client.delete(f"/api/v1/cans-funding-received/{test_funding_received_id}") + + assert response.status_code == 200 + mocker_delete_funding_received.assert_called_once_with(test_funding_received_id) + assert response.json["message"] == "CANFundingReceived deleted" + assert response.json["id"] == test_funding_received_id + + +@pytest.mark.usefixtures("app_ctx") +def test_can_delete_404(budget_team_auth_client): + test_can_id = 600 + + response = budget_team_auth_client.delete(f"/api/v1/cans-funding-received/{test_can_id}") + + assert response.status_code == 404 + + +@pytest.mark.usefixtures("app_ctx") +def test_basic_user_cannot_delete_cans(basic_user_auth_client): + response = basic_user_auth_client.delete("/api/v1/cans-funding-received/517") + + assert response.status_code == 401 + + +def test_service_delete_can(loaded_db): + test_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "Test Notes"} + + funding_received_service = CANFundingReceivedService() + + new_funding_received = funding_received_service.create(test_data) + + funding_received_service.delete(new_funding_received.id) + + stmt = select(CANFundingReceived).where(CANFundingReceived.id == new_funding_received.id) + can = loaded_db.scalar(stmt) + + assert can is None