diff --git a/rctab/routers/accounting/usage.py b/rctab/routers/accounting/usage.py index dfde8a8..7561a8e 100644 --- a/rctab/routers/accounting/usage.py +++ b/rctab/routers/accounting/usage.py @@ -1,6 +1,5 @@ """Set and get usage data.""" -import calendar import datetime import logging from typing import Dict, List @@ -109,19 +108,24 @@ async def post_monthly_usage( """Inserts monthly usage data into the database.""" logger.info("Post monthly usage called") - post_start = datetime.datetime.now() + if len(all_usage.usage_list) == 0: + raise HTTPException( + status_code=400, + detail="Monthly usage data must have at least one record.", + ) - date_min = datetime.date.today() + datetime.timedelta(days=4000) - date_max = datetime.date.today() - datetime.timedelta(days=4000) - monthly_usage = True + post_start = datetime.datetime.now() for usage in all_usage.usage_list: - if usage.date < date_min: - date_min = usage.date - if usage.date > date_max: - date_max = usage.date if usage.monthly_upload is None: - monthly_usage = False + raise HTTPException( + status_code=400, + detail="Post monthly usage data must have the monthly_upload column populated.", + ) + + dates = sorted([x.date for x in all_usage.usage_list]) + date_min = dates[0] + date_max = dates[-1] logger.info( "Post monthly usage received data for %s - %s containing %d records", @@ -130,79 +134,36 @@ async def post_monthly_usage( len(all_usage.usage_list), ) - if date_min.year != date_max.year or date_min.month != date_max.month: - raise HTTPException( - status_code=400, - detail=f"Post monthly usage data should contain usage only for one month. Min, Max usage date: ({str(date_min)}), ({str(date_max)}).", - ) + async with database.transaction(): - if not monthly_usage: - raise HTTPException( - status_code=400, - detail="Post monthly usage data must have the monthly_upload column populated.", + logger.info( + "Post monthly usage deleting existing usage data for %s - %s", + date_min, + date_max, ) - month_start = datetime.date(date_min.year, date_min.month, 1) - month_end = datetime.date( - date_min.year, - date_min.month, - calendar.monthrange(date_min.year, date_min.month)[1], - ) - - logger.info( - "Post monthly usage checks if data for %s - %s has already been posted", - month_start, - month_end, - ) - - # Check if monthly usage has already been posted for the month - query = select([accounting_models.usage]) - query = query.where(accounting_models.usage.c.date >= month_start) - query = query.where(accounting_models.usage.c.date <= month_end) - query = query.where(accounting_models.usage.c.monthly_upload.isnot(None)) - - query_result = await database.fetch_all(query) - - if query_result is not None and len(query_result) > 0: - raise HTTPException( - status_code=400, - detail=f"Post monthly usage data for {str(month_start)}-{str(month_end)} has already been posted.", + # Delete all usage for the time period to have a blank slate. + query_del = ( + accounting_models.usage.delete() + .where(accounting_models.usage.c.date >= date_min) + .where(accounting_models.usage.c.date <= date_max) ) + await database.execute(query_del) - async with UsageEmailContextManager(database): - - async with database.transaction(): - - logger.info( - "Post monthly usage deleting existing usage data for %s - %s", - month_start, - month_end, - ) - - # delete al the usage for the month - query_del = accounting_models.usage.delete().where( - accounting_models.usage.c.date >= month_start - ) - query_del = query_del.where(accounting_models.usage.c.date <= month_end) - await database.execute(query_del) - - logger.info( - "Post monthly usage inserting new subscriptions if they don't exist" - ) - - unique_subscriptions = list( - {i.subscription_id for i in all_usage.usage_list} - ) + logger.info( + "Post monthly usage inserting new subscriptions if they don't exist" + ) - await insert_subscriptions_if_not_exists(unique_subscriptions) + unique_subscriptions = list({i.subscription_id for i in all_usage.usage_list}) - logger.info("Post monthly usage inserting monthly usage data") + await insert_subscriptions_if_not_exists(unique_subscriptions) - await insert_usage(all_usage) + logger.info("Post monthly usage inserting monthly usage data") - logger.info("Post monthly usage refreshing desired states") + await insert_usage(all_usage) - await refresh_desired_states(UUID(ADMIN_OID), unique_subscriptions) + # Note that we don't refresh the desired states here as we don't + # want to trigger excess emails. logger.info("Post monthly usage data took %s", datetime.datetime.now() - post_start) diff --git a/tests/data/example-monthly-wrong.json b/tests/data/example-monthly-wrong.json index 1011426..971278f 100644 --- a/tests/data/example-monthly-wrong.json +++ b/tests/data/example-monthly-wrong.json @@ -206,7 +206,7 @@ "account_name": "JDoe", "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", "subscription_name": "JDoe", - "date": "2021-10-01", + "date": "2021-09-30", "product": "Azure Defender for Resource Manager - Standard - Events", "part_number": "AAH-05467", "meter_id": "f71b1ea3-c809-4ecb-b9d9-ae6b6fa67bd3", diff --git a/tests/data/example-monthly-wrong2.json b/tests/data/example-monthly-wrong2.json deleted file mode 100644 index 971278f..0000000 --- a/tests/data/example-monthly-wrong2.json +++ /dev/null @@ -1,242 +0,0 @@ -[ - { - "id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Billing/billingPeriods/20210801/providers/Microsoft.Consumption/usageDetails/24607b19-d663-8843-97b9-75effac9fb39", - "name": "24607b19-d663-8843-97b9-75effac9fb39", - "type": "Microsoft.Consumption/usageDetails", - "tags": null, - "billing_account_id": "53246346", - "billing_account_name": "Institute", - "billing_period_start_date": "2021-09-01", - "billing_period_end_date": "2021-09-30", - "billing_profile_id": "53246346", - "billing_profile_name": "Institute", - "account_owner_id": "jdoe@domain.ac.uk", - "account_name": "JDoe", - "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", - "subscription_name": "JDoe", - "date": "2021-09-02", - "product": "Azure Defender for Resource Manager - Standard - Events", - "part_number": "AAH-05467", - "meter_id": "f71b1ea3-c809-4ecb-b9d9-ae6b6fa67bd3", - "quantity": 0.000059, - "effective_price": 1.23342584027135, - "cost": 0.000072772124576, - "amortised_cost": 0.0, - "total_cost": 0.000072772124576, - "unit_price": 2.9810000000000003, - "billing_currency": "GBP", - "resource_location": "Unassigned", - "consumed_service": "Microsoft.Security", - "resource_id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Security/pricings/Arm", - "resource_name": "Arm", - "service_info1": null, - "service_info2": null, - "additional_info": null, - "invoice_section": "ASG (SPF EPSRC)", - "cost_center": null, - "resource_group": null, - "reservation_id": null, - "reservation_name": null, - "product_order_id": null, - "offer_id": "MS-AZR-00184", - "is_azure_credit_eligible": true, - "term": null, - "publisher_name": null, - "publisher_type": "Azure", - "plan_name": null, - "charge_type": "Usage", - "frequency": "UsageBased" - }, - { - "id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Billing/billingPeriods/20210801/providers/Microsoft.Consumption/usageDetails/1538963a-0c4b-d98b-304b-3462b47dee3a", - "name": "1538963a-0c4b-d98b-304b-3462b47dee3a", - "type": "Microsoft.Consumption/usageDetails", - "tags": null, - "billing_account_id": "53246346", - "billing_account_name": "Institute", - "billing_period_start_date": "2021-09-01", - "billing_period_end_date": "2021-09-30", - "billing_profile_id": "53246346", - "billing_profile_name": "Institute", - "account_owner_id": "jdoe@domain.ac.uk", - "account_name": "JDoe", - "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", - "subscription_name": "JDoe", - "date": "2021-09-05", - "product": "Azure Defender for Resource Manager - Standard - Events", - "part_number": "AAH-05467", - "meter_id": "f71b1ea3-c809-4ecb-b9d9-ae6b6fa67bd3", - "quantity": 0.000094, - "effective_price": 1.23342584027135, - "cost": 0.000115942028986, - "amortised_cost": 0.0, - "total_cost": 0.000115942028986, - "unit_price": 2.9810000000000003, - "billing_currency": "GBP", - "resource_location": "Unassigned", - "consumed_service": "Microsoft.Security", - "resource_id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Security/pricings/Arm", - "resource_name": "Arm", - "service_info1": null, - "service_info2": null, - "additional_info": null, - "invoice_section": "ASG (SPF EPSRC)", - "cost_center": null, - "resource_group": null, - "reservation_id": null, - "reservation_name": null, - "product_order_id": null, - "offer_id": "MS-AZR-00184", - "is_azure_credit_eligible": true, - "term": null, - "publisher_name": null, - "publisher_type": "Azure", - "plan_name": null, - "charge_type": "Usage", - "frequency": "UsageBased" - }, - { - "id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Billing/billingPeriods/20210801/providers/Microsoft.Consumption/usageDetails/a9fd329b-43ff-bc29-54ae-f3972876ed54", - "name": "a9fd329b-43ff-bc29-54ae-f3972876ed54", - "type": "Microsoft.Consumption/usageDetails", - "tags": null, - "billing_account_id": "53246346", - "billing_account_name": "Institute", - "billing_period_start_date": "2021-09-01", - "billing_period_end_date": "2021-09-30", - "billing_profile_id": "53246346", - "billing_profile_name": "Institute", - "account_owner_id": "jdoe@domain.ac.uk", - "account_name": "JDoe", - "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", - "subscription_name": "JDoe", - "date": "2021-09-15", - "product": "Azure Defender for Resource Manager - Standard - Events", - "part_number": "AAH-05467", - "meter_id": "f71b1ea3-c809-4ecb-b9d9-ae6b6fa67bd3", - "quantity": 0.000059, - "effective_price": 1.23342584027135, - "cost": 0.000072772124576, - "amortised_cost": 0.0, - "total_cost": 0.000072772124576, - "unit_price": 2.9810000000000003, - "billing_currency": "GBP", - "resource_location": "Unassigned", - "consumed_service": "Microsoft.Security", - "resource_id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Security/pricings/Arm", - "resource_name": "Arm", - "service_info1": null, - "service_info2": null, - "additional_info": null, - "invoice_section": "ASG (SPF EPSRC)", - "cost_center": null, - "resource_group": null, - "reservation_id": null, - "reservation_name": null, - "product_order_id": null, - "offer_id": "MS-AZR-00184", - "is_azure_credit_eligible": true, - "term": null, - "publisher_name": null, - "publisher_type": "Azure", - "plan_name": null, - "charge_type": "Usage", - "frequency": "UsageBased" - }, - { - "id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Billing/billingPeriods/20210901/providers/Microsoft.Consumption/usageDetails/0496ffbc-d5d9-a8f6-400d-4ba2c8286270", - "name": "0496ffbc-d5d9-a8f6-400d-4ba2c8286270", - "type": "Microsoft.Consumption/usageDetails", - "tags": null, - "billing_account_id": "53246346", - "billing_account_name": "Institute", - "billing_period_start_date": "2021-09-01", - "billing_period_end_date": "2021-09-30", - "billing_profile_id": "53246346", - "billing_profile_name": "Institute", - "account_owner_id": "jdoe@domain.ac.uk", - "account_name": "JDoe", - "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", - "subscription_name": "JDoe", - "date": "2021-09-30", - "product": "Azure Defender for DNS - Standard - Queries", - "part_number": "AAH-05446", - "meter_id": "58d180f0-a0c8-551f-a33b-7e576895e61a", - "quantity": 0.00007599999999999999, - "effective_price": 0.0, - "cost": 0.0, - "amortised_cost": 0.0, - "total_cost": 0.0, - "unit_price": 0.5217, - "billing_currency": "GBP", - "resource_location": "Unassigned", - "consumed_service": "Microsoft.Security", - "resource_id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Security/pricings/Dns", - "resource_name": "Dns", - "service_info1": null, - "service_info2": null, - "additional_info": null, - "invoice_section": "ASG (SPF EPSRC)", - "cost_center": null, - "resource_group": null, - "reservation_id": null, - "reservation_name": null, - "product_order_id": null, - "offer_id": "MS-AZR-00184", - "is_azure_credit_eligible": true, - "term": null, - "publisher_name": null, - "publisher_type": "Azure", - "plan_name": null, - "charge_type": "Usage", - "frequency": "UsageBased" - }, - { - "id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Billing/billingPeriods/20210901/providers/Microsoft.Consumption/usageDetails/f7b3299d-6379-8de3-3e27-fe03ec7fdb82", - "name": "f7b3299d-6379-8de3-3e27-fe03ec7fdb82", - "type": "Microsoft.Consumption/usageDetails", - "tags": null, - "billing_account_id": "53246346", - "billing_account_name": "Institute", - "billing_period_start_date": "2021-09-01", - "billing_period_end_date": "2021-09-30", - "billing_profile_id": "53246346", - "billing_profile_name": "Institute", - "account_owner_id": "jdoe@domain.ac.uk", - "account_name": "JDoe", - "subscription_id": "ce0f6ae0-2032-11ec-9621-0242ac130002", - "subscription_name": "JDoe", - "date": "2021-09-30", - "product": "Azure Defender for Resource Manager - Standard - Events", - "part_number": "AAH-05467", - "meter_id": "f71b1ea3-c809-4ecb-b9d9-ae6b6fa67bd3", - "quantity": 0.000126, - "effective_price": 1.7299840841464302, - "cost": 0.000217977994602, - "amortised_cost": 0.0, - "total_cost": 0.000217977994602, - "unit_price": 2.9810000000000003, - "billing_currency": "GBP", - "resource_location": "Unassigned", - "consumed_service": "Microsoft.Security", - "resource_id": "/subscriptions/ce0f6ae0-2032-11ec-9621-0242ac130002/providers/Microsoft.Security/pricings/Arm", - "resource_name": "Arm", - "service_info1": null, - "service_info2": null, - "additional_info": null, - "invoice_section": "ASG (SPF EPSRC)", - "cost_center": null, - "resource_group": null, - "reservation_id": null, - "reservation_name": null, - "product_order_id": null, - "offer_id": "MS-AZR-00184", - "is_azure_credit_eligible": true, - "term": null, - "publisher_name": null, - "publisher_type": "Azure", - "plan_name": null, - "charge_type": "Usage", - "frequency": "UsageBased" - } -] diff --git a/tests/test_routes/api_calls.py b/tests/test_routes/api_calls.py index 76a2ef9..c289c30 100644 --- a/tests/test_routes/api_calls.py +++ b/tests/test_routes/api_calls.py @@ -71,7 +71,7 @@ def create_subscription_detail( role_assignments=role_assignments, ) ] - ).json(), + ).model_dump_json(), headers={"authorization": "Bearer " + token}, ) @@ -177,6 +177,6 @@ def create_usage( return client.post( "usage/all-usage", - content=post_data.json(), + content=post_data.model_dump_json(), headers={"authorization": "Bearer " + token}, ) diff --git a/tests/test_routes/test_cost_recovery.py b/tests/test_routes/test_cost_recovery.py index 70cd6f6..3be047d 100644 --- a/tests/test_routes/test_cost_recovery.py +++ b/tests/test_routes/test_cost_recovery.py @@ -43,7 +43,7 @@ def test_cost_recovery_app_route( recovery_period = CostRecoveryMonth(first_day=date(year=2001, month=1, day=1)) result = client.post( PREFIX + "/app-cost-recovery", - content=recovery_period.json(), + content=recovery_period.model_dump_json(), headers={"authorization": "Bearer " + token}, ) @@ -73,7 +73,7 @@ def test_cost_recovery_cli_route( recovery_period = CostRecoveryMonth(first_day=date(year=2001, month=1, day=1)) result = client.post( PREFIX + "/cli-cost-recovery", - content=recovery_period.json(), + content=recovery_period.model_dump_json(), ) mock.assert_called_once_with( @@ -99,7 +99,7 @@ def test_cost_recovery_cli_route_dry_run( result = client.request( "GET", PREFIX + "/cli-cost-recovery", - content=recovery_period.json(), + content=recovery_period.model_dump_json(), ) mock.assert_called_once_with( diff --git a/tests/test_routes/test_finances.py b/tests/test_routes/test_finances.py index 2aeac53..074610b 100644 --- a/tests/test_routes/test_finances.py +++ b/tests/test_routes/test_finances.py @@ -46,7 +46,9 @@ def test_finance_route(auth_app: FastAPI) -> None: result = client.request( "GET", PREFIX + "/finance", - content=SubscriptionItem(sub_id=UUID(int=33)).json(), + content=SubscriptionItem(sub_id=UUID(int=33)) + .model_dump_json() + .encode("utf-8"), ) assert result.status_code == 200 @@ -117,7 +119,7 @@ def test_finances_route(auth_app: FastAPI) -> None: finance_code="test_finance", priority=1, ) - result = client.post(PREFIX + "/finances", content=f_a.json()) + result = client.post(PREFIX + "/finances", content=f_a.model_dump_json()) assert result.status_code == 201 @@ -364,13 +366,14 @@ def test_finance_post_get_put_delete(auth_app: FastAPI) -> None: finance_code="test_finance", priority=1, ) - result = client.post(PREFIX + "/finances", content=f_a.json()) + result = client.post(PREFIX + "/finances", content=f_a.model_dump_json()) assert result.status_code == 201 f_a_returned = FinanceWithID.parse_raw(result.content) f_a_returned.amount = 10.0 result = client.put( - PREFIX + f"/finances/{f_a_returned.id}", content=f_a_returned.json() + PREFIX + f"/finances/{f_a_returned.id}", + content=f_a_returned.model_dump_json(), ) assert result.status_code == 200 @@ -381,7 +384,7 @@ def test_finance_post_get_put_delete(auth_app: FastAPI) -> None: result = client.request( "DELETE", PREFIX + f"/finances/{f_a_returned.id}", - content=SubscriptionItem(sub_id=constants.TEST_SUB_UUID).json(), + content=SubscriptionItem(sub_id=constants.TEST_SUB_UUID).model_dump_json(), ) assert result.status_code == 200 @@ -545,19 +548,20 @@ def test_finance_can_update(auth_app: FastAPI) -> None: finance_code="test_finance", priority=1, ) - result = client.post(PREFIX + "/finances", content=f_a.json()) + result = client.post(PREFIX + "/finances", content=f_a.model_dump_json()) assert result.status_code == 201 f_a_returned = FinanceWithID.parse_raw(result.content) result = client.post( PREFIX + "/cli-cost-recovery", - content=CostRecoveryMonth(first_day="2022-09-01").json(), + content=CostRecoveryMonth(first_day="2022-09-01").model_dump_json(), ) assert result.status_code == 200 f_a_returned.amount = 10.0 result = client.put( - PREFIX + f"/finances/{f_a_returned.id}", content=f_a_returned.json() + PREFIX + f"/finances/{f_a_returned.id}", + content=f_a_returned.model_dump_json(), ) assert result.status_code == 200 diff --git a/tests/test_routes/test_status.py b/tests/test_routes/test_status.py index 09adbe6..e828bef 100644 --- a/tests/test_routes/test_status.py +++ b/tests/test_routes/test_status.py @@ -58,7 +58,7 @@ def test_post_status( resp = client.post( "accounting/all-status", - content=all_status.json(), + content=all_status.model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 200 @@ -72,7 +72,7 @@ def test_post_status( # Check that we can POST the same status again without issue (idempotency). resp = client.post( "accounting/all-status", - content=all_status.json(), + content=all_status.model_dump_json(), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 200 diff --git a/tests/test_routes/test_usage.py b/tests/test_routes/test_usage.py index 2ff569d..5570fcb 100644 --- a/tests/test_routes/test_usage.py +++ b/tests/test_routes/test_usage.py @@ -19,14 +19,18 @@ BillingStatus, CMUsage, SubscriptionDetails, + Usage, ) from rctab.constants import ADMIN_OID, EMAIL_TYPE_USAGE_ALERT from rctab.crud.accounting_models import usage_view from rctab.crud.models import database -from rctab.routers.accounting.usage import post_usage +from rctab.routers.accounting.usage import get_usage, post_monthly_usage, post_usage from tests.test_routes import api_calls, constants -from tests.test_routes.test_routes import test_db # pylint: disable=unused-import +from tests.test_routes.test_routes import ( # pylint: disable=unused-import + create_subscription, + test_db, +) from tests.utils import print_list_diff date_from = datetime.date.today() @@ -53,7 +57,7 @@ def test_post_usage( resp = client.post( "usage/all-usage", - content=post_data.json(), + content=post_data.model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) @@ -221,7 +225,7 @@ def _post_costmanagement( post_client = client.post( "/usage/all-cm-usage", headers={"authorization": "Bearer " + token}, - content=all_usage.json(), + content=all_usage.model_dump_json().encode("utf-8"), ) # type: ignore return post_client # type: ignore @@ -279,18 +283,15 @@ def test_post_monthly_usage( mocker: pytest_mock.MockerFixture, ) -> None: auth_app, token = app_with_signed_billing_token + example_1_file = Path("tests/data/example-monthly-wrong.json") example_1_data = json.loads(example_1_file.read_text(encoding="utf-8")) - example_2_file = Path("tests/data/example-monthly-wrong2.json") + example_2_file = Path("tests/data/example-monthly-correct.json") example_2_data = json.loads(example_2_file.read_text(encoding="utf-8")) - example_3_file = Path("tests/data/example-monthly-correct.json") - example_3_data = json.loads(example_3_file.read_text(encoding="utf-8")) - post_example_1_data = AllUsage(usage_list=example_1_data) post_example_2_data = AllUsage(usage_list=example_2_data) - post_example_3_data = AllUsage(usage_list=example_3_data) with TestClient(auth_app) as client: mock_refresh = AsyncMock() @@ -298,37 +299,33 @@ def test_post_monthly_usage( "rctab.routers.accounting.usage.refresh_desired_states", mock_refresh ) + # Should error if there is no data. resp = client.post( "usage/monthly-usage", - content=post_example_1_data.json(), + content=AllUsage(usage_list=[]).model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 400 + assert "must have at least one record" in resp.text resp = client.post( "usage/monthly-usage", - content=post_example_2_data.json(), + content=post_example_1_data.model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 400 + assert "data must have the monthly_upload column" in resp.text resp = client.post( "usage/monthly-usage", - content=post_example_3_data.json(), + content=post_example_2_data.model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 200 - # Posting the usage data should have the side effect of - # refreshing the desired states - mock_refresh.assert_called_once_with( - UUID(ADMIN_OID), - list({x.subscription_id for x in post_example_3_data.usage_list}), - ) - get_resp = client.get( "usage/all-usage", headers={"authorization": "Bearer " + token}, @@ -339,10 +336,97 @@ def test_post_monthly_usage( resp_data = get_resp.json() assert np.isclose( sum(i["total_cost"] for i in resp_data), - sum(i.total_cost for i in post_example_3_data.usage_list), + sum(i.total_cost for i in post_example_2_data.usage_list), ) +@pytest.mark.asyncio +async def test_monthly_usage_2( + test_db: Database, # pylint: disable=redefined-outer-name +) -> None: + sub1 = await create_subscription(test_db) + sub2 = await create_subscription(test_db) + + await post_usage( + AllUsage( + usage_list=[ + Usage( + id=str(UUID(int=0)), + subscription_id=sub1, + date="2024-04-01", + total_cost=1.0, + invoice_section="-", + ), + Usage( + id=str(UUID(int=1)), + subscription_id=sub2, + date="2024-04-02", + total_cost=2.0, + invoice_section="-", + ), + Usage( + id=str(UUID(int=2)), + subscription_id=sub1, + date="2024-04-03", + total_cost=4.0, + invoice_section="-", + ), + ] + ), + {"mock": "authentication"}, + ) + + await post_monthly_usage( + AllUsage( + usage_list=[ + Usage( + id=str(UUID(int=3)), + subscription_id=sub1, + date="2024-04-01", + total_cost=10.0, + invoice_section="-", + monthly_upload=datetime.date.today(), + ), + Usage( + id=str(UUID(int=4)), + subscription_id=sub1, + date="2024-04-02", + total_cost=0.5, + invoice_section="-", + monthly_upload=datetime.date.today(), + ), + ] + ), + {"mock": "authentication"}, + ) + all_usage = await get_usage() + assert all_usage == [ + Usage( + id=str(UUID(int=2)), + subscription_id=sub1, + date="2024-04-03", + total_cost=4.0, + invoice_section="-", + ), + Usage( + id=str(UUID(int=3)), + subscription_id=sub1, + date="2024-04-01", + total_cost=10.0, + invoice_section="-", + monthly_upload=datetime.date.today(), + ), + Usage( + id=str(UUID(int=4)), + subscription_id=sub1, + date="2024-04-02", + total_cost=0.5, + invoice_section="-", + monthly_upload=datetime.date.today(), + ), + ] + + @pytest.mark.asyncio async def test_post_usage_refreshes_view( test_db: Database, mocker: MockerFixture # pylint: disable=redefined-outer-name @@ -383,7 +467,7 @@ def test_post_usage_emails( resp = client.post( "usage/all-usage", - content=post_data.json(), + content=post_data.model_dump_json().encode("utf-8"), headers={"authorization": "Bearer " + token}, ) assert resp.status_code == 200