|
| 1 | +import pytest |
| 2 | +from decimal import Decimal |
| 3 | +from unittest.mock import patch |
| 4 | +from compliance.models import ( |
| 5 | + ComplianceEarnedCredit, |
| 6 | + ComplianceReportVersion, |
| 7 | + ComplianceReportVersionManualHandling, |
| 8 | + ElicensingLineItem, |
| 9 | + ElicensingPayment, |
| 10 | +) |
| 11 | +from compliance.tests.integration.utils import ( |
| 12 | + ComplianceIntegrationTestBase, |
| 13 | + HANDLE_OBLIGATION_INTEGRATION, |
| 14 | + REFRESH_DATA, |
| 15 | + SEND_OBLIGATION_EMAIL, |
| 16 | + SEND_EARNED_CREDITS_EMAIL, |
| 17 | + SEND_NO_OBLIGATION_EMAIL, |
| 18 | +) |
| 19 | + |
| 20 | +ON_COMMIT_PATH = "django.db.transaction.on_commit" |
| 21 | +CREATE_ADJUSTMENT_PATH = ( |
| 22 | + "compliance.service.compliance_adjustment_service" |
| 23 | + ".ComplianceAdjustmentService.create_adjustment_for_target_version" |
| 24 | +) |
| 25 | + |
| 26 | +pytestmark = pytest.mark.django_db |
| 27 | + |
| 28 | + |
| 29 | +class TestManualHandlingSupplementary(ComplianceIntegrationTestBase): |
| 30 | + REPORTING_YEAR = 2024 |
| 31 | + |
| 32 | + def _approve_initial_earned_credits(self): |
| 33 | + """Simulate credit approval.""" |
| 34 | + self.initial_earned_credit.issuance_status = ComplianceEarnedCredit.IssuanceStatus.APPROVED |
| 35 | + self.initial_earned_credit.bccr_trading_name = "Test Trading Name" |
| 36 | + self.initial_earned_credit.bccr_holding_account_id = "TEST-001" |
| 37 | + self.initial_earned_credit.save( |
| 38 | + update_fields=["issuance_status", "bccr_trading_name", "bccr_holding_account_id"] |
| 39 | + ) |
| 40 | + |
| 41 | + # ------------------------------------------------------------------ |
| 42 | + # Scenario 1: Excess emissions decrease after invoice has been paid |
| 43 | + # ------------------------------------------------------------------ |
| 44 | + |
| 45 | + @patch(SEND_NO_OBLIGATION_EMAIL) |
| 46 | + @patch(SEND_OBLIGATION_EMAIL) |
| 47 | + @patch(CREATE_ADJUSTMENT_PATH) |
| 48 | + @patch(ON_COMMIT_PATH) |
| 49 | + @patch(REFRESH_DATA) |
| 50 | + @patch(HANDLE_OBLIGATION_INTEGRATION) |
| 51 | + def test_decreased_obligation_paid_invoice_creates_manual_handling( |
| 52 | + self, mock_elicensing, mock_refresh, mock_on_commit, _mock_create_adj, _mock_obl_email, _mock_no_obl_email |
| 53 | + ): |
| 54 | + """Excess emissions decrease when invoice has been partially paid with cash. |
| 55 | +
|
| 56 | + Routing: |
| 57 | + - ManualHandler: no manual handling on initial CRV → can't handle |
| 58 | + - SupercedeVersionHandler: invoice exists (not pre-invoice) → can't handle |
| 59 | + - DecreasedObligationHandler: excess decreased + unpaid invoice + cash paid → CAN handle |
| 60 | +
|
| 61 | + Setup: |
| 62 | + - Initial: 10 excess emissions, invoice created with outstanding = full fee |
| 63 | + - Half the fee is paid in cash; outstanding_balance is reduced to half |
| 64 | + - Supplementary: 0 excess → refund_pool = full fee > outstanding (half fee) |
| 65 | + → obligation fully paid AND leftover refund > 0 AND has_cash → manual handling |
| 66 | +
|
| 67 | + Expected: |
| 68 | + - New CRV with NO_OBLIGATION_OR_EARNED_CREDITS status |
| 69 | + - Previous CRV marked OBLIGATION_FULLY_MET |
| 70 | + - ManualHandling record created with handling_type=OBLIGATION, |
| 71 | + context=OBLIGATION_REFUND_POOL_CASH |
| 72 | + """ |
| 73 | + mock_elicensing.side_effect = self.fake_handle_obligation_post_invoice |
| 74 | + mock_refresh.side_effect = self.fake_refresh |
| 75 | + mock_on_commit.side_effect = lambda fn: fn() |
| 76 | + |
| 77 | + self._create_base_infrastructure() |
| 78 | + self._create_initial_report_with_obligation_post_invoice(Decimal("10")) |
| 79 | + |
| 80 | + invoice = self.initial_obligation.elicensing_invoice |
| 81 | + fee = invoice.outstanding_balance |
| 82 | + partial_payment = (fee / 2).quantize(Decimal("0.01")) |
| 83 | + invoice.outstanding_balance = fee - partial_payment |
| 84 | + invoice.save(update_fields=["outstanding_balance"]) |
| 85 | + |
| 86 | + fee_line_item = ElicensingLineItem.objects.get( |
| 87 | + elicensing_invoice=invoice, |
| 88 | + line_item_type=ElicensingLineItem.LineItemType.FEE, |
| 89 | + ) |
| 90 | + ElicensingPayment.objects.create( |
| 91 | + payment_object_id=self.initial_obligation.id, |
| 92 | + elicensing_line_item=fee_line_item, |
| 93 | + amount=partial_payment, |
| 94 | + ) |
| 95 | + |
| 96 | + self._submit_supplementary_report( |
| 97 | + excess_emissions=Decimal("0"), |
| 98 | + previous_report_version=self.initial_report_version, |
| 99 | + ) |
| 100 | + |
| 101 | + supp_crv = ComplianceReportVersion.objects.get( |
| 102 | + compliance_report=self.compliance_report, |
| 103 | + is_supplementary=True, |
| 104 | + ) |
| 105 | + assert supp_crv.status == ComplianceReportVersion.ComplianceStatus.NO_OBLIGATION_OR_EARNED_CREDITS |
| 106 | + assert supp_crv.previous_version == self.initial_crv |
| 107 | + |
| 108 | + manual_handling = ComplianceReportVersionManualHandling.objects.get(compliance_report_version=supp_crv) |
| 109 | + assert manual_handling.handling_type == ComplianceReportVersionManualHandling.HandlingType.OBLIGATION |
| 110 | + assert manual_handling.context == ComplianceReportVersionManualHandling.Context.OBLIGATION_REFUND_POOL_CASH |
| 111 | + assert manual_handling.director_decision == ( |
| 112 | + ComplianceReportVersionManualHandling.DirectorDecision.PENDING_MANUAL_HANDLING |
| 113 | + ) |
| 114 | + |
| 115 | + self.initial_crv.refresh_from_db() |
| 116 | + assert self.initial_crv.status == ComplianceReportVersion.ComplianceStatus.OBLIGATION_FULLY_MET |
| 117 | + |
| 118 | + # ------------------------------------------------------------------ |
| 119 | + # Scenario 2: Credited emissions decrease after credits have been approved |
| 120 | + # ------------------------------------------------------------------ |
| 121 | + |
| 122 | + @patch(SEND_NO_OBLIGATION_EMAIL) |
| 123 | + @patch(SEND_EARNED_CREDITS_EMAIL) |
| 124 | + @patch(SEND_OBLIGATION_EMAIL) |
| 125 | + @patch(HANDLE_OBLIGATION_INTEGRATION) |
| 126 | + def test_decreased_credits_approved_creates_manual_handling( |
| 127 | + self, _mock_elicensing, _mock_obl_email, _mock_ec_email, _mock_no_obl_email |
| 128 | + ): |
| 129 | + """Credited emissions decrease when credits are APPROVED. |
| 130 | +
|
| 131 | + Routing: |
| 132 | + - ManualHandler: no manual handling on initial CRV → can't handle |
| 133 | + - SupercedeVersionHandler: issuance_status is APPROVED (not CREDITS_NOT_ISSUED) → can't handle |
| 134 | + - DecreasedCreditHandler: credited decreased + previous credit exists → CAN handle (APPROVED branch) |
| 135 | +
|
| 136 | + Expected: |
| 137 | + - New CRV with EARNED_CREDITS status |
| 138 | + - ManualHandling record created with handling_type=EARNED_CREDITS, |
| 139 | + context=EARNED_CREDITS_PREVIOUSLY_APPROVED |
| 140 | + - Previous earned credit NOT modified (stays APPROVED) |
| 141 | + """ |
| 142 | + self._create_base_infrastructure() |
| 143 | + self._create_initial_report_with_earned_credits(Decimal("100")) |
| 144 | + self._approve_initial_earned_credits() |
| 145 | + |
| 146 | + self._submit_supplementary_report( |
| 147 | + excess_emissions=Decimal("0"), |
| 148 | + credited_emissions=Decimal("50"), |
| 149 | + previous_report_version=self.initial_report_version, |
| 150 | + ) |
| 151 | + |
| 152 | + supp_crv = ComplianceReportVersion.objects.get( |
| 153 | + compliance_report=self.compliance_report, |
| 154 | + is_supplementary=True, |
| 155 | + ) |
| 156 | + assert supp_crv.status == ComplianceReportVersion.ComplianceStatus.EARNED_CREDITS |
| 157 | + assert supp_crv.previous_version == self.initial_crv |
| 158 | + |
| 159 | + manual_handling = ComplianceReportVersionManualHandling.objects.get(compliance_report_version=supp_crv) |
| 160 | + assert manual_handling.handling_type == ComplianceReportVersionManualHandling.HandlingType.EARNED_CREDITS |
| 161 | + assert ( |
| 162 | + manual_handling.context == ComplianceReportVersionManualHandling.Context.EARNED_CREDITS_PREVIOUSLY_APPROVED |
| 163 | + ) |
| 164 | + assert manual_handling.director_decision == ( |
| 165 | + ComplianceReportVersionManualHandling.DirectorDecision.PENDING_MANUAL_HANDLING |
| 166 | + ) |
| 167 | + |
| 168 | + self.initial_earned_credit.refresh_from_db() |
| 169 | + assert self.initial_earned_credit.issuance_status == ComplianceEarnedCredit.IssuanceStatus.APPROVED |
0 commit comments