diff --git a/api/config.py b/api/config.py index c3b52dee3..40f38d066 100644 --- a/api/config.py +++ b/api/config.py @@ -142,7 +142,10 @@ class TestConfig(Config): # pylint: disable=too-few-public-methods DB_NAME = os.getenv('DATABASE_TEST_NAME', 'unittesting') DB_HOST = os.getenv('DATABASE_TEST_HOST', 'localhost') DB_PORT = os.getenv('DATABASE_TEST_PORT', '54345') - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + if os.getenv('TEST_USE_LOCAL_DB', False): + SQLALCHEMY_DATABASE_URI = f'postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + else: + SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' # Ensure SQLAlchemy is properly configured for Flask-Marshmallow compatibility SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/api/namex/analytics/solr.py b/api/namex/analytics/solr.py index 1d31990b0..95fc4862d 100644 --- a/api/namex/analytics/solr.py +++ b/api/namex/analytics/solr.py @@ -1279,14 +1279,14 @@ def name_pre_processing(cls, name): .replace('$', 's') .replace(' ¢ ', 'cent') .replace('¢', 'c') - .replace('britishcolumbia', 'bc') - .replace('britishcolumbias', 'bc') - .replace('britishcolumbian', 'bc') - .replace('britishcolumbians', 'bc') - .replace('british columbia', 'bc') - .replace('british columbias', 'bc') - .replace('british columbian', 'bc') - .replace('british columbians', 'bc') + # .replace('britishcolumbia', 'bc') + # .replace('britishcolumbias', 'bc') + # .replace('britishcolumbian', 'bc') + # .replace('britishcolumbians', 'bc') + # .replace('british columbia', 'bc') + # .replace('british columbias', 'bc') + # .replace('british columbian', 'bc') + # .replace('british columbians', 'bc') ) return processed_name.strip() diff --git a/api/namex/services/payment/models/__init__.py b/api/namex/services/payment/models/__init__.py index 391a956b5..efacaf86f 100644 --- a/api/namex/services/payment/models/__init__.py +++ b/api/namex/services/payment/models/__init__.py @@ -1,5 +1,9 @@ -import dataclasses from dataclasses import dataclass, field +from typing import Optional, Union +from decimal import Decimal + +from pydantic.dataclasses import dataclass as pydantic_dataclass +from pydantic import Field from datetime import date from .abstract import Serializable @@ -118,7 +122,21 @@ class PaymentRequest(Serializable): details: list = field(default_factory=PaymentDetailItem) -@dataclass +class PydanticConfig: + """Pydantic config to ignore extra fields.""" + extra = 'ignore' + underscore_attrs_are_private = False + + +@pydantic_dataclass(config=PydanticConfig) +class PaymentRefundInvoice: + refundId: int + refundAmount: Decimal + message: str + isPartialRefund: bool + + +@pydantic_dataclass(config=PydanticConfig) class PaymentInvoice(Serializable): id: int serviceFees: float @@ -140,25 +158,20 @@ class PaymentInvoice(Serializable): routingSlip: str = '' datNumber: str = '' folioNumber: str = '' - lineItems: list = field(default_factory=list) - receipts: list = field(default_factory=list) - references: list = field(default_factory=list) - details: list = field(default_factory=list) - _links: list = field(default_factory=list) - paymentAccount: dict = field(default_factory=dict) - - def __init__(self, **kwargs): - """Set the attributes only if the field is defined.""" - self.lineItems = [] - self.receipts = [] - self.references = [] - self.details = [] - self._links = [] - self.paymentAccount = {} - names = {f.name for f in dataclasses.fields(self)} - for k, v in kwargs.items(): - if k in names: - setattr(self, k, v) + lineItems: list = Field(default_factory=list) + receipts: list = Field(default_factory=list) + references: list = Field(default_factory=list) + details: list = Field(default_factory=list) + links: list = Field(default_factory=list) + paymentAccount: dict = Field(default_factory=dict) + + @property + def _links(self) -> list: + return self.links + + @_links.setter + def _links(self, value: list) -> None: + self.links = value @dataclass @@ -180,11 +193,11 @@ class Receipt(Serializable): receiptNumber: str = '' -@dataclass +@pydantic_dataclass(config=PydanticConfig) class ReceiptResponse(Serializable): - bcOnlineAccountNumber: str = None - filingIdentifier: str = None - invoice: PaymentInvoice = field(default=PaymentInvoice) + bcOnlineAccountNumber: Optional[str] = None + filingIdentifier: Optional[str] = None + invoice: Optional[Union[PaymentInvoice, dict]] = None invoiceNumber: str = '' paymentMethod: str = '' receiptNumber: str = '' diff --git a/api/namex/services/payment/payments.py b/api/namex/services/payment/payments.py index b85a1a36e..075313b65 100644 --- a/api/namex/services/payment/payments.py +++ b/api/namex/services/payment/payments.py @@ -4,7 +4,7 @@ from .client import SBCPaymentClient from .exceptions import SBCPaymentException -from .models import PaymentInvoice +from .models import PaymentInvoice, PaymentRefundInvoice def get_payment(payment_identifier): @@ -38,8 +38,12 @@ def refund_payment(payment_identifier, model=None): data = model api_instance = SBCPaymentClient() api_response = api_instance.refund_payment(payment_identifier, data) - current_app.logger.debug(api_response) - return PaymentInvoice(**api_response) if api_response else None + current_app.logger.debug( + "services refund_payment response", + payment_identifier=payment_identifier, + api_response=api_response, + ) + return PaymentRefundInvoice(**api_response) if api_response else None except Exception as err: raise SBCPaymentException(err) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index e4988246e..1726baa6b 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -69,7 +69,6 @@ def client_ctx(app): with app.test_client() as c: yield c - @pytest.fixture(scope='session') def db(app, request): """ diff --git a/api/tests/python/end_points/payments/test_payments.py b/api/tests/python/end_points/payments/test_payments.py index f0fab4fc9..d5ebd4b41 100644 --- a/api/tests/python/end_points/payments/test_payments.py +++ b/api/tests/python/end_points/payments/test_payments.py @@ -589,6 +589,26 @@ def mock_publish(topic: str, payload: bytes): 'total': 31.5, }, ) + + # Mock the get_payment API call that happens during payment completion + mocker.patch.object( + SBCPaymentClient, + 'get_payment', + return_value={ + 'id': 1, + 'serviceFees': 1.5, + 'paid': 31.5, + 'refund': 0.0, + 'total': 31.5, + 'isPaymentActionRequired': False, + 'statusCode': 'CREATED', + 'businessIdentifier': 'NR L000001', + 'createdOn': '2021-01-14T23:52:05.531317+00:00', + 'lineItems': [{'filingTypeCode': 'NM620', 'priority': False, 'waiveFees': False}], + 'references': [], + }, + ) + payment = execute_payment(client, jwt, create_payment_request, action) assert payment['action'] == action if complete_payment: diff --git a/api/tests/python/models/test_payment_invoice.py b/api/tests/python/models/test_payment_invoice.py new file mode 100644 index 000000000..8e5687a44 --- /dev/null +++ b/api/tests/python/models/test_payment_invoice.py @@ -0,0 +1,237 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for PaymentInvoice dataclass.""" + +import pytest + +from namex.services.payment.models import PaymentInvoice + + +def test_init_with_valid_fields(): + """Assert that valid fields are set correctly.""" + data = { + 'id': 12345, + 'serviceFees': 1.50, + 'paid': 30.0, + 'refund': 0.0, + 'total': 31.50, + 'statusCode': 'COMPLETED', + 'paymentMethod': 'CC', + 'businessIdentifier': 'NR L000001' + } + invoice = PaymentInvoice(**data) + + assert invoice.id == data['id'] + assert invoice.serviceFees == data['serviceFees'] + assert invoice.paid == data['paid'] + assert invoice.refund == data['refund'] + assert invoice.total == data['total'] + assert invoice.statusCode == data['statusCode'] + assert invoice.paymentMethod == data['paymentMethod'] + assert invoice.businessIdentifier == data['businessIdentifier'] + + +def test_init_ignores_invalid_fields(): + """Assert that unknown fields are ignored.""" + data = { + 'id': 100, + 'serviceFees': 1.0, + 'paid': 10.0, + 'refund': 0.0, + 'total': 11.0, + 'unknownField': 'should-be-ignored', + 'anotherInvalid': 12345 + } + invoice = PaymentInvoice(**data) + + assert invoice.id == 100 + assert not hasattr(invoice, 'unknownField') + assert not hasattr(invoice, 'anotherInvalid') + + +def test_init_with_default_list_fields(): + """Assert that list fields default to empty lists.""" + data = { + 'id': 200, + 'serviceFees': 1.0, + 'paid': 20.0, + 'refund': 0.0, + 'total': 21.0 + } + invoice = PaymentInvoice(**data) + + assert invoice.lineItems == [] + assert invoice.receipts == [] + assert invoice.references == [] + assert invoice.details == [] + assert invoice._links == [] + + +def test_init_with_default_dict_field(): + """Assert that paymentAccount defaults to empty dict.""" + data = { + 'id': 300, + 'serviceFees': 1.0, + 'paid': 30.0, + 'refund': 0.0, + 'total': 31.0 + } + invoice = PaymentInvoice(**data) + + assert invoice.paymentAccount == {} + + +def test_init_with_payment_account_data(): + """Assert that paymentAccount field accepts dict data.""" + data = { + 'id': 400, + 'serviceFees': 1.5, + 'paid': 40.0, + 'refund': 0.0, + 'total': 41.5, + 'paymentAccount': {'accountId': '2698', 'accountName': 'online banking'} + } + invoice = PaymentInvoice(**data) + + assert invoice.paymentAccount == {'accountId': '2698', 'accountName': 'online banking'} + + +def test_init_with_partial_data(): + """Assert that partial data sets only provided fields.""" + data = { + 'id': 500, + 'serviceFees': 1.0, + 'paid': 50.0, + 'refund': 0.0, + 'total': 51.0, + 'statusCode': 'CREATED' + } + invoice = PaymentInvoice(**data) + + assert invoice.id == 500 + assert invoice.statusCode == 'CREATED' + assert invoice.paymentMethod == '' + assert invoice.businessIdentifier == '' + assert invoice.bcolAccount is None + + +def test_init_with_bcol_account(): + """Assert that bcolAccount field is set correctly.""" + data = { + 'id': 600, + 'serviceFees': 1.0, + 'paid': 60.0, + 'refund': 0.0, + 'total': 61.0, + 'bcolAccount': 123456 + } + invoice = PaymentInvoice(**data) + + assert invoice.bcolAccount == 123456 + + +def test_init_with_is_payment_action_required(): + """Assert that isPaymentActionRequired field defaults correctly.""" + data = { + 'id': 700, + 'serviceFees': 1.0, + 'paid': 70.0, + 'refund': 0.0, + 'total': 71.0 + } + invoice = PaymentInvoice(**data) + + assert invoice.isPaymentActionRequired is False + + +def test_init_with_is_payment_action_required_true(): + """Assert that isPaymentActionRequired can be set to True.""" + data = { + 'id': 800, + 'serviceFees': 1.0, + 'paid': 0.0, + 'refund': 0.0, + 'total': 81.0, + 'isPaymentActionRequired': True + } + invoice = PaymentInvoice(**data) + + assert invoice.isPaymentActionRequired is True + + +def test_init_with_line_items(): + """Assert that lineItems field accepts list data.""" + line_items = [{'description': 'Name Request', 'amount': 30.0}] + data = { + 'id': 900, + 'serviceFees': 1.0, + 'paid': 31.0, + 'refund': 0.0, + 'total': 31.0, + 'lineItems': line_items + } + invoice = PaymentInvoice(**data) + + assert invoice.lineItems == line_items + + +def test_init_with_all_string_fields(): + """Assert that all string fields are set correctly.""" + data = { + 'id': 1000, + 'serviceFees': 1.0, + 'paid': 100.0, + 'refund': 0.0, + 'total': 101.0, + 'statusCode': 'COMPLETED', + 'createdBy': 'user1', + 'createdName': 'Test User', + 'createdOn': '2026-01-15T10:00:00', + 'updatedBy': 'user2', + 'updatedName': 'Update User', + 'updatedOn': '2026-01-15T11:00:00', + 'paymentMethod': 'ONLINE_BANKING', + 'businessIdentifier': 'NR L000002', + 'corpTypeCode': 'NRO', + 'routingSlip': 'RS-001', + 'datNumber': 'DAT-001', + 'folioNumber': 'FOL-001' + } + invoice = PaymentInvoice(**data) + + assert invoice.statusCode == 'COMPLETED' + assert invoice.createdBy == 'user1' + assert invoice.createdName == 'Test User' + assert invoice.createdOn == '2026-01-15T10:00:00' + assert invoice.updatedBy == 'user2' + assert invoice.updatedName == 'Update User' + assert invoice.updatedOn == '2026-01-15T11:00:00' + assert invoice.paymentMethod == 'ONLINE_BANKING' + assert invoice.businessIdentifier == 'NR L000002' + assert invoice.corpTypeCode == 'NRO' + assert invoice.routingSlip == 'RS-001' + assert invoice.datNumber == 'DAT-001' + assert invoice.folioNumber == 'FOL-001' + + +def test_init_missing_mandatory_fields(): + """Assert that missing mandatory fields raises validation error.""" + data = { + 'statusCode': 'COMPLETED', + 'paymentMethod': 'CC' + } + + with pytest.raises(Exception): + PaymentInvoice(**data) + diff --git a/api/tests/python/models/test_receipt_response.py b/api/tests/python/models/test_receipt_response.py new file mode 100644 index 000000000..bfa9955cd --- /dev/null +++ b/api/tests/python/models/test_receipt_response.py @@ -0,0 +1,115 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for ReceiptResponse dataclass.""" + +import pytest + +from namex.services.payment.models import PaymentInvoice, ReceiptResponse + +invoice = {'id': 100, 'total': 30.0, 'paid': 30.0, 'refund': 0.0, 'serviceFees': 1.5} + +def test_init_with_valid_fields(): + """Assert that valid fields are set correctly.""" + data = { + 'bcOnlineAccountNumber': 'BC12345', + 'filingIdentifier': 'FIL-001', + 'invoice': invoice, + 'invoiceNumber': 'INV-100', + 'paymentMethod': 'CC', + 'receiptNumber': 'REC-999', + 'routingSlipNumber': 'RS-001' + } + response = ReceiptResponse(**data) + + assert response.bcOnlineAccountNumber == 'BC12345' + assert response.filingIdentifier == 'FIL-001' + assert response.invoiceNumber == 'INV-100' + assert response.paymentMethod == 'CC' + assert response.receiptNumber == 'REC-999' + assert response.routingSlipNumber == 'RS-001' + +def test_init_ignores_invalid_fields(): + """Assert that unknown fields are ignored.""" + data = { + 'invoice': invoice, + 'receiptNumber': 'REC-123', + 'unknownField': 'should-be-ignored', + 'anotherInvalid': 12345 + } + response = ReceiptResponse(**data) + + assert response.receiptNumber == 'REC-123' + assert not hasattr(response, 'unknownField') + assert not hasattr(response, 'anotherInvalid') + +def test_init_with_empty_kwargs(): + """Assert that empty kwargs results in default values.""" + response = ReceiptResponse() + + assert response.bcOnlineAccountNumber is None + assert response.filingIdentifier is None + assert response.invoiceNumber == '' + assert response.paymentMethod == '' + assert response.receiptNumber == '' + assert response.routingSlipNumber == '' + +def test_init_with_partial_data(): + """Assert that partial data sets only provided fields.""" + data = { + 'receiptNumber': 'REC-PARTIAL', + 'paymentMethod': 'DIRECT_PAY' + } + response = ReceiptResponse(**data) + + assert response.receiptNumber == 'REC-PARTIAL' + assert response.paymentMethod == 'DIRECT_PAY' + assert response.bcOnlineAccountNumber is None + assert response.invoiceNumber == '' + +def test_init_with_invoice_dict(): + """Assert that invoice field accepts dict data.""" + invoice_data = {'id': 100, 'total': 30.0, 'paid': 30.0} + data = { + 'receiptNumber': 'REC-INV', + 'invoice': invoice_data + } + response = ReceiptResponse(**data) + + assert response.receiptNumber == 'REC-INV' + assert response.invoice == invoice_data + +def test_init_overwrites_none_defaults(): + """Assert that None defaults can be overwritten.""" + data = { + 'bcOnlineAccountNumber': 'BCOL-NEW', + 'filingIdentifier': 'FIL-NEW' + } + response = ReceiptResponse(**data) + + assert response.bcOnlineAccountNumber == 'BCOL-NEW' + assert response.filingIdentifier == 'FIL-NEW' + +def test_init_with_missing_filing_identifier(): + """Assert that missing filingIdentifier results in None default.""" + data = { + 'receiptNumber': 'REC-NO-FILING', + 'paymentMethod': 'CC', + 'invoiceNumber': 'INV-200' + } + response = ReceiptResponse(**data) + + assert response.receiptNumber == 'REC-NO-FILING' + assert response.paymentMethod == 'CC' + assert response.invoiceNumber == 'INV-200' + assert response.filingIdentifier is None diff --git a/api/tests/python/services/payment/test_models.py b/api/tests/python/services/payment/test_models.py index adc8c2225..9ef88c255 100644 --- a/api/tests/python/services/payment/test_models.py +++ b/api/tests/python/services/payment/test_models.py @@ -21,6 +21,7 @@ def test_payment_invoice_model(session): 'corpTypeCode': 'NRO', 'id': 11801, 'paid': 0.0, + 'refund': 0.0, 'paymentAccount': {'accountId': '2698', 'accountName': 'online banking 13.1'}, 'paymentMethod': 'ONLINE_BANKING', 'serviceFees': 1.5, diff --git a/api/tests/python/services/payment/test_refund_payment.py b/api/tests/python/services/payment/test_refund_payment.py new file mode 100644 index 000000000..419d5bcc9 --- /dev/null +++ b/api/tests/python/services/payment/test_refund_payment.py @@ -0,0 +1,105 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for PaymentRefundInvoice dataclass.""" + +from decimal import Decimal +from unittest.mock import patch, MagicMock + +import pytest +from namex.services.payment.exceptions import SBCPaymentException +from namex.services.payment.models import PaymentRefundInvoice +from namex.services.payment.payments import refund_payment + + +@pytest.fixture +def mock_client(): + with patch('namex.services.payment.payments.SBCPaymentClient') as mock_client: + yield mock_client + + +@pytest.fixture +def valid_response_data(): + return { + "refundId": 1234, + "refundAmount": Decimal("100.00"), + "message": "Refund processed successfully.", + "isPartialRefund": False + } + + +def test_refund_payment_success(mock_client, valid_response_data): + mock_instance = MagicMock() + mock_client.return_value = mock_instance + mock_instance.refund_payment.return_value = valid_response_data + + payment_identifier = "valid-payment-id" + model = {"reason": "Customer request"} + + response = refund_payment(payment_identifier, model) + + expected_response = PaymentRefundInvoice(**valid_response_data) + assert response == expected_response + mock_instance.refund_payment.assert_called_once_with(payment_identifier, model) + + +def test_refund_payment_no_response(mock_client): + mock_instance = MagicMock() + mock_client.return_value = mock_instance + mock_instance.refund_payment.return_value = None + + payment_identifier = "valid-payment-id" + model = {"reason": "Customer request"} + + response = refund_payment(payment_identifier, model) + + assert response is None + mock_instance.refund_payment.assert_called_once_with(payment_identifier, model) + + +def test_refund_payment_raises_exception(mock_client): + mock_instance = MagicMock() + mock_client.return_value = mock_instance + mock_instance.refund_payment.side_effect = Exception("SBC Pay API exception.") + + payment_identifier = "invalid-payment-id" + model = {"reason": "Invalid request"} + + with pytest.raises(SBCPaymentException, match="SBC Pay API exception."): + refund_payment(payment_identifier, model) + mock_instance.refund_payment.assert_called_once_with(payment_identifier, model) + + +def test_refund_payment_with_extra_response_data(mock_client, valid_response_data): + mock_instance = MagicMock() + mock_client.return_value = mock_instance + + response_with_extra_fields = { + **valid_response_data, + "extraField": "should-be-ignored", + "anotherUnknownField": 9999, + "nestedExtra": {"key": "value"} + } + mock_instance.refund_payment.return_value = response_with_extra_fields + + payment_identifier = "valid-payment-id" + model = {"reason": "Customer request"} + + response = refund_payment(payment_identifier, model) + + expected_response = PaymentRefundInvoice(**valid_response_data) + assert response == expected_response + assert not hasattr(response, 'extraField') + assert not hasattr(response, 'anotherUnknownField') + assert not hasattr(response, 'nestedExtra') + mock_instance.refund_payment.assert_called_once_with(payment_identifier, model) diff --git a/services/namex-pay/config.py b/services/namex-pay/config.py index 846e4ce9d..a724efc26 100644 --- a/services/namex-pay/config.py +++ b/services/namex-pay/config.py @@ -109,7 +109,11 @@ class TestConfig(Config): # pylint: disable=too-few-public-methods DB_NAME = os.getenv('DATABASE_TEST_NAME', 'unittesting') DB_HOST = os.getenv('DATABASE_TEST_HOST', 'localhost') DB_PORT = os.getenv('DATABASE_TEST_PORT', '5432') - SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + if os.getenv('TEST_USE_LOCAL_DB', False): + SQLALCHEMY_DATABASE_URI = f'postgresql+psycopg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + else: + SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + EMAILER_TOPIC = os.getenv('NAMEX_MAILER_TOPIC', '') NAMEX_NR_STATE_TOPIC = os.getenv('NAMEX_NR_STATE_TOPIC', '') diff --git a/services/namex-pay/src/namex_pay/resources/worker.py b/services/namex-pay/src/namex_pay/resources/worker.py index 33ddd3b73..ddffaf91f 100644 --- a/services/namex-pay/src/namex_pay/resources/worker.py +++ b/services/namex-pay/src/namex_pay/resources/worker.py @@ -17,7 +17,6 @@ """ import time -from dataclasses import dataclass from datetime import timedelta from enum import Enum from http import HTTPStatus @@ -29,6 +28,7 @@ from namex.models import Request as RequestDAO # noqa:I001; import orders from namex.services import EventRecorder, queue # noqa:I005; from namex.services.name_request.name_request_state import is_reapplication_eligible +from pydantic.dataclasses import dataclass as pydantic_dataclass from sbc_common_components.utils.enums import QueueMessageTypes from simple_cloudevent import SimpleCloudEvent from sqlalchemy.exc import OperationalError @@ -95,7 +95,12 @@ def worker(): return ret # noqa: B012 -@dataclass +class PydanticConfig: + """Pydantic config to ignore extra fields.""" + extra = 'ignore' + + +@pydantic_dataclass(config=PydanticConfig) class PaymentToken: """Payment Token class""" diff --git a/services/namex-pay/tests/unit/test_payment_token.py b/services/namex-pay/tests/unit/test_payment_token.py new file mode 100644 index 000000000..dd1ac9982 --- /dev/null +++ b/services/namex-pay/tests/unit/test_payment_token.py @@ -0,0 +1,109 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for PaymentToken dataclass.""" + +import pytest + +from namex_pay.resources.worker import PaymentToken + + +def test_init_with_all_fields(): + """Assert that all valid fields are set correctly.""" + data = { + 'id': 'PAY-12345', + 'status_code': 'COMPLETED', + 'filing_identifier': 'FIL-001', + 'corp_type_code': 'BC' + } + token = PaymentToken(**data) + + assert token.id == 'PAY-12345' + assert token.status_code == 'COMPLETED' + assert token.filing_identifier == 'FIL-001' + assert token.corp_type_code == 'BC' + +def test_init_with_partial_fields(): + """Assert that partial data sets only provided fields.""" + data = { + 'id': 'PAY-999', + 'status_code': 'PENDING' + } + token = PaymentToken(**data) + + assert token.id == 'PAY-999' + assert token.status_code == 'PENDING' + assert token.filing_identifier is None + assert token.corp_type_code is None + +def test_init_ignores_invalid_fields(): + """Assert that unknown fields are ignored.""" + data = { + 'id': 'PAY-123', + 'status_code': 'COMPLETED', + 'unknown_field': 'should-be-ignored', + 'another_invalid': 12345 + } + token = PaymentToken(**data) + + assert token.id == 'PAY-123' + assert token.status_code == 'COMPLETED' + assert not hasattr(token, 'unknown_field') + assert not hasattr(token, 'another_invalid') + +def test_init_with_empty_kwargs(): + """Assert that empty kwargs results in default None values.""" + token = PaymentToken() + + assert token.id is None + assert token.status_code is None + assert token.filing_identifier is None + assert token.corp_type_code is None + +def test_init_with_none_values(): + """Assert that explicit None values are set correctly.""" + data = { + 'id': 'PAY-001', + 'status_code': None, + 'filing_identifier': None, + 'corp_type_code': 'NRO' + } + token = PaymentToken(**data) + + assert token.id == 'PAY-001' + assert token.status_code is None + assert token.filing_identifier is None + assert token.corp_type_code == 'NRO' + +@pytest.mark.parametrize('status_code', [ + 'COMPLETED', + 'APPROVED', + 'PENDING', + 'TRANSACTION_FAILED', +]) +def test_init_with_various_status_codes(status_code): + """Assert that different status codes are accepted.""" + token = PaymentToken(id='PAY-001', status_code=status_code) + + assert token.status_code == status_code + +def test_init_with_integer_id(): + """Assert that integer id values are accepted.""" + data = { + 'id': 29590, + 'status_code': 'COMPLETED' + } + token = PaymentToken(**data) + + assert token.id == '29590' + assert token.status_code == 'COMPLETED' diff --git a/services/namex-pay/tests/unit/test_worker.py b/services/namex-pay/tests/unit/test_worker.py index 070f23c7f..ec6bfe5ef 100644 --- a/services/namex-pay/tests/unit/test_worker.py +++ b/services/namex-pay/tests/unit/test_worker.py @@ -87,7 +87,7 @@ def test_get_payment_token(): ce = SimpleCloudEvent(**ce_dict) payment_token = get_payment_token(ce) assert payment_token - assert payment_token.id == ce_dict['data']['id'] + assert payment_token.id == str(ce_dict['data']['id']) # wrong type ce_dict = deepcopy(CLOUD_EVENT_TEMPLATE)