diff --git a/docs/docs/PayBC Mocking/paybc-1.0.0.yaml b/docs/docs/PayBC Mocking/paybc-1.0.0.yaml index 3ecfd65db..b4f926b57 100644 --- a/docs/docs/PayBC Mocking/paybc-1.0.0.yaml +++ b/docs/docs/PayBC Mocking/paybc-1.0.0.yaml @@ -119,11 +119,13 @@ revenueamount: '20' glstatus: PAID glerrormessage: null + refundglstatus: PAID - linenumber: '2' revenueaccount: 112.32562.20245.4378.3212319.000000.0000 revenueamount: '30' glstatus: PAID glerrormessage: null + refundglstatus: PAID '400': description: BadRequest content: diff --git a/jobs/payment-jobs/tasks/common/dataclasses.py b/jobs/payment-jobs/tasks/common/dataclasses.py index b5bdda484..c6da9ec17 100644 --- a/jobs/payment-jobs/tasks/common/dataclasses.py +++ b/jobs/payment-jobs/tasks/common/dataclasses.py @@ -18,7 +18,7 @@ from pay_api.models import Invoice as InvoiceModel from pay_api.models import PaymentLineItem as LineItemModel from pay_api.utils.enums import InvoiceStatus -from tasks.common.enums import PaymentDetailsGlStatus, PaymentDetailsStatus +from tasks.common.enums import PaymentDetailsGlStatus @dataclass @@ -33,7 +33,6 @@ class RevenueLine(JSONWizard): class OrderStatus(JSONWizard): # pylint:disable=too-many-instance-attributes """Return from order status query.""" - refundstatus: Optional[PaymentDetailsStatus] revenue: List[RevenueLine] diff --git a/jobs/payment-jobs/tasks/common/enums.py b/jobs/payment-jobs/tasks/common/enums.py index 2068e1cb7..7861b409b 100644 --- a/jobs/payment-jobs/tasks/common/enums.py +++ b/jobs/payment-jobs/tasks/common/enums.py @@ -15,14 +15,6 @@ from enum import Enum -class PaymentDetailsStatus(Enum): - """Payment details status.""" - - REFUND_INPRG = 'REFUND_INPRG' - PAID = 'PAID' - CMPLT = 'CMPLT' - - class PaymentDetailsGlStatus(Enum): """Payment details GL status.""" diff --git a/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py b/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py index fbe07eae4..9feef1995 100644 --- a/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py +++ b/jobs/payment-jobs/tasks/direct_pay_automated_refund_task.py @@ -27,7 +27,7 @@ from sentry_sdk import capture_message from tasks.common.dataclasses import OrderStatus -from tasks.common.enums import PaymentDetailsGlStatus, PaymentDetailsStatus +from tasks.common.enums import PaymentDetailsGlStatus class DirectPayAutomatedRefundTask: # pylint:disable=too-few-public-methods @@ -54,12 +54,12 @@ def handle_non_complete_credit_card_refunds(cls): excluding invoices with refunds that have gl_posted or gl_error. Initial state for refunds is REFUND INPRG. 2. Get order status for CFS (refundstatus, revenue.refundglstatus) - 2.1. Check for refundStatus = PAID and invoice = REFUND_REQUESTED: + 2.1. Check for all revenue.refundGLstatus = PAID and invoice = REFUND_REQUESTED: Set invoice and payment = REFUNDED - 2.2. Check for refundStatus = CMPLT or None (None is for refunds done manually) + 2.2. Check for all revenue.refundGLstatus = CMPLT Set invoice and payment = REFUNDED (incase we skipped the condition above). Set refund.gl_posted = now() - 2.3. Check for refundGLstatus = RJCT + 2.3. Check for any revenue.refundGLstatus = RJCT Log the error down, contact PAYBC if this happens. Set refund.gl_error = """ @@ -82,9 +82,6 @@ def handle_non_complete_credit_card_refunds(cls): elif cls._is_status_paid_and_invoice_refund_requested(status, invoice): cls._refund_paid(invoice) elif cls._is_status_complete(status): - if status.refundstatus is None: - current_app.logger.info( - 'Refund status was blank, setting to complete - this was an existing manual refund.') cls._refund_complete(invoice) else: current_app.logger.info('No action taken for invoice.') @@ -144,13 +141,15 @@ def _is_glstatus_rejected(status: OrderStatus) -> bool: @staticmethod def _is_status_paid_and_invoice_refund_requested(status: OrderStatus, invoice: Invoice) -> bool: """Check for successful refund and invoice status = REFUND_REQUESTED.""" - return status.refundstatus == PaymentDetailsStatus.PAID \ + return all(line.refundglstatus == PaymentDetailsGlStatus.PAID + for line in status.revenue) \ and invoice.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value @staticmethod def _is_status_complete(status: OrderStatus) -> bool: - """Check for successful refund, or if the refund was done manually.""" - return status.refundstatus == PaymentDetailsStatus.CMPLT or status.refundstatus is None + """Check for successful refund.""" + return all(line.refundglstatus == PaymentDetailsGlStatus.CMPLT + for line in status.revenue) @staticmethod def _set_invoice_and_payment_to_refunded(invoice: Invoice): diff --git a/jobs/payment-jobs/tasks/distribution_task.py b/jobs/payment-jobs/tasks/distribution_task.py index 4d8cfd171..e5ee85fa8 100644 --- a/jobs/payment-jobs/tasks/distribution_task.py +++ b/jobs/payment-jobs/tasks/distribution_task.py @@ -75,7 +75,7 @@ def update_failed_distributions(cls): # pylint:disable=too-many-locals continue target_status, target_gl_status = cls.get_status_fields(gl_update_invoice.invoice_status_code) - if payment_details.get(target_status) == STATUS_PAID: + if target_status is None or payment_details.get(target_status) == STATUS_PAID: has_gl_completed: bool = True for revenue in payment_details.get('revenue'): if revenue.get(target_gl_status) in STATUS_NOT_PROCESSED: @@ -88,7 +88,8 @@ def update_failed_distributions(cls): # pylint:disable=too-many-locals def get_status_fields(cls, invoice_status_code: str) -> tuple: """Get status fields for invoice status code.""" if invoice_status_code == InvoiceStatus.UPDATE_REVENUE_ACCOUNT_REFUND.value: - return 'refundstatus', 'refundglstatus' + # Refund doesn't use a top level status, as partial refunds may occur. + return None, 'refundglstatus' return 'paymentstatus', 'glstatus' @classmethod diff --git a/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py b/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py index d08cae7c9..545d19ee0 100644 --- a/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py +++ b/jobs/payment-jobs/tests/jobs/test_direct_pay_automated_refund_task.py @@ -41,8 +41,12 @@ def test_successful_paid_refund(session, monkeypatch): def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods return { - 'refundstatus': 'PAID', - 'revenue': [] + 'revenue': [ + { + 'refundglstatus': 'PAID', + 'refundglerrormessage': '' + } + ] } target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' monkeypatch.setattr(target, payment_status) @@ -63,8 +67,12 @@ def test_successful_completed_refund(session, monkeypatch): def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods return { - 'refundstatus': 'CMPLT', - 'revenue': [] + 'revenue': [ + { + 'refundglstatus': 'CMPLT', + 'refundglerrormessage': '' + } + ] } target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' monkeypatch.setattr(target, payment_status) @@ -86,7 +94,6 @@ def test_bad_cfs_refund(session, monkeypatch): def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods return { - 'refundstatus': 'PAID', 'revenue': [ { 'linenumber': '1', @@ -115,45 +122,3 @@ def payment_status(cls): # pylint: disable=unused-argument; mocks of library me DirectPayAutomatedRefundTask().process_cc_refunds() assert refund.gl_error == 'BAD BAD' assert refund.gl_posted is None - - -def test_manual_refund(session, monkeypatch): - """Assert manual refunds get set to REFUNDED.""" - invoice = factory_invoice(factory_create_direct_pay_account(), status_code=InvoiceStatus.REFUNDED.value) - factory_invoice_reference(invoice.id, invoice.id, InvoiceReferenceStatus.COMPLETED.value).save() - payment = factory_payment('PAYBC', invoice_number=invoice.id) - refund = factory_refund_invoice(invoice.id) - - def payment_status(cls): # pylint: disable=unused-argument; mocks of library methods - return { - 'refundstatus': None, - 'revenue': [ - { - 'linenumber': '1', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '130', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refundglstatus': None, - 'refundglerrormessage': None - }, - { - 'linenumber': '2', - 'revenueaccount': '112.32041.35301.1278.3200000.000000.0000', - 'revenueamount': '1.5', - 'glstatus': 'PAID', - 'glerrormessage': None, - 'refundglstatus': None, - 'refundglerrormessage': None - } - ] - } - - target = 'tasks.direct_pay_automated_refund_task.DirectPayAutomatedRefundTask._query_order_status' - monkeypatch.setattr(target, payment_status) - with freeze_time(datetime.datetime.combine(datetime.datetime.utcnow().date(), datetime.time(6, 00))): - DirectPayAutomatedRefundTask().process_cc_refunds() - assert invoice.invoice_status_code == InvoiceStatus.REFUNDED.value - assert invoice.refund_date is not None - assert payment.payment_status_code == PaymentStatus.REFUNDED.value - assert refund.gl_posted is not None diff --git a/pay-api/src/pay_api/services/direct_pay_service.py b/pay-api/src/pay_api/services/direct_pay_service.py index f7b67c280..66ac13473 100644 --- a/pay-api/src/pay_api/services/direct_pay_service.py +++ b/pay-api/src/pay_api/services/direct_pay_service.py @@ -238,6 +238,11 @@ def _build_automated_refund_payload(invoice: InvoiceModel): receipt = ReceiptModel.find_by_invoice_id_and_receipt_number(invoice_id=invoice.id) invoice_reference = InvoiceReferenceModel.find_by_invoice_id_and_status( invoice.id, InvoiceReferenceStatus.COMPLETED.value) + # Future: Partial refund support - This is backwards compatible + # refundRevenue: [{ + # 'lineNumber': 1, + # 'refundAmount': 50.00, + # }] return { 'orderNumber': int(receipt.receipt_number), 'pbcRefNumber': current_app.config.get('PAYBC_DIRECT_PAY_REF_NUMBER'),