@@ -598,189 +598,126 @@ async def test_handle_submitted_no_sign(
598598
599599
600600@pytest .mark .anyio
601- async def test_handle_submitted_status_no_credits (
601+ async def test_handle_submitted_status_skips_reserve_without_pre_deadline_balance (
602602 compliance_report_update_service ,
603- mock_repo ,
604603 mock_user_has_roles ,
605604 mock_org_service ,
606605 mock_summary_service ,
607- mock_summary_repo ,
608606):
609- """
610- Scenario: The report requires deficit units to be reserved (-100),
611- but available_balance is 0, so no transaction is created.
612- """
613- report_id = 1
614- mock_report = MagicMock (spec = ComplianceReport )
615- mock_report .compliance_report_id = report_id
616- mock_report .organization_id = 123
617- mock_report .summary = None
618- # No existing transaction
619- mock_report .transaction = None
620-
621- # Required roles are present
607+ """Scenario: No eligible credits exist before the deadline, so no reserve is created."""
622608 mock_user_has_roles .return_value = True
623- compliance_report_update_service .request = MagicMock ()
624- compliance_report_update_service .request .user = MagicMock ()
609+ report = MagicMock (spec = ComplianceReport )
610+ report .compliance_report_id = 1
611+ report .organization_id = 123
612+ report .compliance_period = MagicMock (description = "2024" )
613+ report .summary = MagicMock (spec = ComplianceReportSummary )
614+ report .summary .line_20_surplus_deficit_units = - 150
615+ report .transaction = None
625616
626- # Mock the summary so we skip deeper logic
627- mock_summary_repo .get_summary_by_report_id .return_value = None
628-
629- # Mock calculated summary - should be model-like object with line_20_surplus_deficit_units
630- calculated_summary = MagicMock (spec = ComplianceReportSummary )
631- calculated_summary .line_20_surplus_deficit_units = - 100
632- mock_summary_service .calculate_compliance_report_summary = AsyncMock (
633- return_value = calculated_summary
617+ mock_summary_service .calculate_compliance_report_summary .return_value = (
618+ report .summary
634619 )
635- # available_balance = 0
636- mock_org_service .calculate_available_balance .return_value = 0
637- # If adjust_balance is called, we'll see an assertion fail
638- mock_org_service .adjust_balance = AsyncMock ()
620+ mock_org_service .calculate_available_balance .return_value = 100000
621+ mock_org_service .calculate_available_balance_for_period .return_value = 0
639622
640- # Execute
641623 await compliance_report_update_service .handle_submitted_status (
642- mock_report , UserProfile ()
624+ report , UserProfile ()
643625 )
644626
645- # Assertions:
646- # 1) Summary was assigned and calculated twice (once for creation, once for recalculation)
647- assert mock_report .summary == calculated_summary
648- assert mock_summary_service .calculate_compliance_report_summary .call_count == 2
649- # 2) We did NOT call adjust_balance, because balance = 0
650627 mock_org_service .adjust_balance .assert_not_awaited ()
651- # 3) No transaction is created
652- assert mock_report .transaction is None
628+ assert report .transaction is None
629+ mock_org_service .calculate_available_balance .assert_awaited_once_with (
630+ report .organization_id
631+ )
632+ mock_org_service .calculate_available_balance_for_period .assert_awaited_once_with (
633+ report .organization_id , 2024
634+ )
653635
654636
655637@pytest .mark .anyio
656- async def test_handle_submitted_status_insufficient_credits (
638+ async def test_handle_submitted_status_caps_to_pre_deadline_balance (
657639 compliance_report_update_service ,
658- mock_repo ,
659- mock_summary_repo ,
660640 mock_user_has_roles ,
661641 mock_org_service ,
662642 mock_summary_service ,
663643):
664- """
665- Scenario: The report requires deficit units of 100,
666- but the org only has 50 credits available. We reserve partial (-50)
667- to match the actual available balance.
668- """
669- report_id = 1
670- mock_report = MagicMock (spec = ComplianceReport )
671- mock_report .compliance_report_id = report_id
672- mock_report .organization_id = 123
673- # Need 100 credits, but only 50 are available
674- mock_report .summary = MagicMock (spec = ComplianceReportSummary )
675- mock_report .summary .line_20_surplus_deficit_units = - 100
676- mock_report .transaction = None
677-
644+ """Scenario: Eligible credits before the deadline cap the reserve even though the live balance is higher."""
678645 mock_user_has_roles .return_value = True
679- compliance_report_update_service .request = MagicMock ()
680- compliance_report_update_service .request .user = MagicMock ()
681-
682- # Skip deeper summary logic
683- mock_summary_repo .get_summary_by_report_id .return_value = None
684- mock_summary_repo .save_compliance_report_summary = AsyncMock (
685- return_value = mock_report .summary
686- )
687- mock_summary_repo .add_compliance_report_summary = AsyncMock (
688- return_value = mock_report .summary
689- )
690- calculated_summary = ComplianceReportSummarySchema (
691- can_sign = True ,
692- compliance_report_id = report_id ,
693- renewable_fuel_target_summary = [],
694- low_carbon_fuel_target_summary = [],
695- non_compliance_penalty_summary = [],
696- )
697- mock_summary_service .calculate_compliance_report_summary = AsyncMock (
698- return_value = calculated_summary
699- )
700-
701- # Org only has 50
702- mock_org_service .calculate_available_balance = AsyncMock (return_value = 50 )
703- # Mock the result of adjust_balance
646+ report = MagicMock (spec = ComplianceReport )
647+ report .compliance_report_id = 2
648+ report .organization_id = 321
649+ report .compliance_period = MagicMock (description = "2024" )
650+ report .summary = MagicMock (spec = ComplianceReportSummary )
651+ report .summary .line_20_surplus_deficit_units = - 120000
652+ report .transaction = None
653+
654+ mock_summary_service .calculate_compliance_report_summary .return_value = (
655+ report .summary
656+ )
657+ mock_org_service .calculate_available_balance .return_value = 120000
658+ mock_org_service .calculate_available_balance_for_period .return_value = 80000
704659 mock_transaction = MagicMock ()
705660 mock_org_service .adjust_balance .return_value = mock_transaction
706661
707- # Execute
708662 await compliance_report_update_service .handle_submitted_status (
709- mock_report , UserProfile ()
663+ report , UserProfile ()
710664 )
711665
712- # We should have called adjust_balance with -50 units (reserving partial)
713666 mock_org_service .adjust_balance .assert_awaited_once_with (
714667 transaction_action = TransactionActionEnum .Reserved ,
715- compliance_units = - 50 ,
716- organization_id = 123 ,
668+ compliance_units = - 80000 ,
669+ organization_id = 321 ,
670+ )
671+ assert report .transaction is mock_transaction
672+ mock_org_service .calculate_available_balance .assert_awaited_once_with (
673+ report .organization_id
674+ )
675+ mock_org_service .calculate_available_balance_for_period .assert_awaited_once_with (
676+ report .organization_id , 2024
717677 )
718- # And a transaction object is assigned back to the report
719- assert mock_report .transaction == mock_transaction
720678
721679
722680@pytest .mark .anyio
723- async def test_handle_submitted_status_sufficient_credits (
681+ async def test_handle_submitted_status_caps_to_live_balance_when_smaller (
724682 compliance_report_update_service ,
725- mock_repo ,
726- mock_summary_repo ,
727683 mock_user_has_roles ,
728684 mock_org_service ,
729685 mock_summary_service ,
730686):
731- """
732- Scenario: The report requires deficit units of -100,
733- and the org has 200 credits available. We reserve all -100.
734- """
735- report_id = 1
736- mock_report = MagicMock (spec = ComplianceReport )
737- mock_report .compliance_report_id = report_id
738- mock_report .organization_id = 123
739- # Need 100 credits
740- mock_report .summary = MagicMock (spec = ComplianceReportSummary )
741- mock_report .summary .line_20_surplus_deficit_units = - 100
742- mock_report .transaction = None
743-
687+ """Scenario: Live balance is lower than pre-deadline total, so reserve is limited by current availability."""
744688 mock_user_has_roles .return_value = True
745- compliance_report_update_service .request = MagicMock ()
746- compliance_report_update_service .request .user = MagicMock ()
747-
748- # Skip deeper summary logic
749- mock_summary_repo .get_summary_by_report_id .return_value = None
750- mock_summary_repo .save_compliance_report_summary = AsyncMock (
751- return_value = mock_report .summary
752- )
753- mock_summary_repo .add_compliance_report_summary = AsyncMock (
754- return_value = mock_report .summary
755- )
756- calculated_summary = ComplianceReportSummarySchema (
757- can_sign = True ,
758- compliance_report_id = report_id ,
759- renewable_fuel_target_summary = [],
760- low_carbon_fuel_target_summary = [],
761- non_compliance_penalty_summary = [],
762- )
763- mock_summary_service .calculate_compliance_report_summary = AsyncMock (
764- return_value = calculated_summary
765- )
766-
767- # Org has enough
768- mock_org_service .calculate_available_balance .return_value = 200
689+ report = MagicMock (spec = ComplianceReport )
690+ report .compliance_report_id = 3
691+ report .organization_id = 555
692+ report .compliance_period = MagicMock (description = "2024" )
693+ report .summary = MagicMock (spec = ComplianceReportSummary )
694+ report .summary .line_20_surplus_deficit_units = - 120000
695+ report .transaction = None
696+
697+ mock_summary_service .calculate_compliance_report_summary .return_value = (
698+ report .summary
699+ )
700+ mock_org_service .calculate_available_balance .return_value = 40000
701+ mock_org_service .calculate_available_balance_for_period .return_value = 100000
769702 mock_transaction = MagicMock ()
770703 mock_org_service .adjust_balance .return_value = mock_transaction
771704
772- # Execute
773705 await compliance_report_update_service .handle_submitted_status (
774- mock_report , UserProfile ()
706+ report , UserProfile ()
775707 )
776708
777- # We should have called adjust_balance with the full -100
778709 mock_org_service .adjust_balance .assert_awaited_once_with (
779710 transaction_action = TransactionActionEnum .Reserved ,
780- compliance_units = - 100 ,
781- organization_id = 123 ,
711+ compliance_units = - 40000 ,
712+ organization_id = 555 ,
713+ )
714+ assert report .transaction is mock_transaction
715+ mock_org_service .calculate_available_balance .assert_awaited_once_with (
716+ report .organization_id
717+ )
718+ mock_org_service .calculate_available_balance_for_period .assert_awaited_once_with (
719+ report .organization_id , 2024
782720 )
783- assert mock_report .transaction == mock_transaction
784721
785722
786723# Fixture to create a real instance of OrganizationsService with its actual adjust_balance logic.
@@ -959,8 +896,10 @@ async def test_handle_assessed_status_not_superseded(
959896 mock_report_model .version = mock_compliance_report_assessed .version
960897 # Set a mock transaction object on the model
961898 mock_report_model .transaction = MagicMock ()
899+ mock_report_model .transaction .compliance_units = 100
962900 # Set is_non_assessment to False to enter transaction logic
963901 mock_report_model .is_non_assessment = False
902+ mock_report_model .compliance_period = MagicMock (description = "2024" )
964903 # Mock the summary that should already be locked from "Recommended by Analyst" step
965904 mock_summary = MagicMock ()
966905 mock_summary .line_20_surplus_deficit_units = 100
@@ -997,6 +936,52 @@ async def test_handle_assessed_status_not_superseded(
997936 mock_repo .update_compliance_report .assert_called_once_with (mock_report_model )
998937
999938
939+ @pytest .mark .anyio
940+ async def test_handle_assessed_status_caps_to_pre_deadline (
941+ compliance_report_update_service : ComplianceReportUpdateService ,
942+ mock_repo : AsyncMock ,
943+ mock_user_profile_director : MagicMock ,
944+ ):
945+ mock_report = MagicMock (spec = ComplianceReport )
946+ mock_report .compliance_report_id = 999
947+ mock_report .compliance_report_group_uuid = "group-999"
948+ mock_report .version = 1
949+ mock_report .organization_id = 321
950+ mock_report .transaction = MagicMock ()
951+ mock_report .is_non_assessment = False
952+ mock_report .compliance_period = MagicMock (description = "2024" )
953+ mock_report .transaction .compliance_units = - 200
954+ mock_report .compliance_period = MagicMock (description = "2024" )
955+
956+ mock_summary = MagicMock ()
957+ mock_summary .line_20_surplus_deficit_units = - 300
958+ mock_summary .is_locked = True
959+ mock_report .summary = mock_summary
960+
961+ mock_repo .get_draft_report_by_group_uuid = AsyncMock (return_value = None )
962+
963+ compliance_report_update_service .org_service .calculate_available_balance .return_value = 500
964+ compliance_report_update_service .org_service .calculate_available_balance_for_period .return_value = 200
965+
966+ with patch (
967+ "lcfs.web.api.compliance_report.update_service.user_has_roles" ,
968+ return_value = True ,
969+ ):
970+ await compliance_report_update_service .handle_assessed_status (
971+ mock_report , mock_user_profile_director
972+ )
973+
974+ compliance_report_update_service .org_service .calculate_available_balance .assert_not_awaited ()
975+ compliance_report_update_service .org_service .calculate_available_balance_for_period .assert_not_awaited ()
976+ assert mock_report .transaction .transaction_action == TransactionActionEnum .Adjustment
977+ assert mock_report .transaction .compliance_units == - 200
978+ assert (
979+ mock_report .transaction .update_user
980+ == mock_user_profile_director .keycloak_username
981+ )
982+ mock_repo .update_compliance_report .assert_called_once_with (mock_report )
983+
984+
1000985@pytest .mark .anyio
1001986async def test_handle_assessed_status_government_adjustment_no_transaction (
1002987 compliance_report_update_service : ComplianceReportUpdateService ,
@@ -1021,6 +1006,7 @@ async def test_handle_assessed_status_government_adjustment_no_transaction(
10211006 mock_report .transaction = None # No existing transaction - the key bug case
10221007 # Set is_non_assessment to False to enter transaction logic
10231008 mock_report .is_non_assessment = False
1009+ mock_report .compliance_period = MagicMock (description = "2024" )
10241010
10251011 # Set up supplemental initiator to indicate it's a government adjustment
10261012 mock_report .supplemental_initiator = (
@@ -1042,7 +1028,7 @@ async def test_handle_assessed_status_government_adjustment_no_transaction(
10421028 # This is needed to simulate the transaction being created
10431029 def side_effect_create_transaction (credit_change , report ):
10441030 report .transaction = mock_transaction
1045- return mock_transaction
1031+ return credit_change
10461032
10471033 compliance_report_update_service ._create_or_update_reserve_transaction .side_effect = (
10481034 side_effect_create_transaction
@@ -1259,6 +1245,8 @@ async def test_handle_assessed_status_calls_calculate_with_skip_check(
12591245 mock_report .version = 1
12601246 mock_report .transaction = MagicMock ()
12611247 mock_report .is_non_assessment = False
1248+ mock_report .compliance_period = MagicMock (description = "2024" )
1249+ mock_report .transaction .compliance_units = 150
12621250
12631251 # Mock the summary that should already be locked
12641252 mock_summary = MagicMock (spec = ComplianceReportSummary )
0 commit comments