diff --git a/pay-api/flags.json b/pay-api/flags.json index c0d3793d4..870b306f4 100644 --- a/pay-api/flags.json +++ b/pay-api/flags.json @@ -2,6 +2,7 @@ "flagValues": { "string-flag": "a string value", "bool-flag": true, - "integer-flag": 10 + "integer-flag": 10, + "enable-eft-payment-method": true } } diff --git a/pay-api/migrations/versions/2023_10_18_194cdd7cf986_17829_eft_shortnames.py b/pay-api/migrations/versions/2023_10_18_194cdd7cf986_17829_eft_shortnames.py new file mode 100644 index 000000000..2c667f971 --- /dev/null +++ b/pay-api/migrations/versions/2023_10_18_194cdd7cf986_17829_eft_shortnames.py @@ -0,0 +1,32 @@ +"""17829-eft-shortnames + +Revision ID: 194cdd7cf986 +Revises: 456234145e5e +Create Date: 2023-10-18 08:23:04.207463 + +""" +from alembic import op +import sqlalchemy as sa + +from pay_api import db + +# revision identifiers, used by Alembic. +revision = '194cdd7cf986' +down_revision = '456234145e5e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('eft_short_names', + sa.Column('id', sa.Integer(), nullable=False, primary_key=True), + sa.Column('short_name', sa.String(), nullable=False, unique=True), + sa.Column('auth_account_id', sa.String(length=50), nullable=True), + sa.Column('created_on', sa.DateTime(), nullable=False), + ) + + op.create_index(op.f('ix_eft_short_names_auth_account_id'), 'eft_short_names', ['auth_account_id'], unique=False) + + +def downgrade(): + op.drop_table('eft_short_names') diff --git a/pay-api/src/pay_api/models/eft_short_names.py b/pay-api/src/pay_api/models/eft_short_names.py new file mode 100644 index 000000000..d0f58932b --- /dev/null +++ b/pay-api/src/pay_api/models/eft_short_names.py @@ -0,0 +1,48 @@ +# Copyright © 2023 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. +"""Model to handle EFT TDI17 short name to BCROS account mapping.""" + +from datetime import datetime + +from .base_model import BaseModel +from .db import db + + +class EFTShortnames(BaseModel): # pylint: disable=too-many-instance-attributes + """This class manages the EFT short name to auth account mapping.""" + + __tablename__ = 'eft_short_names' + # this mapper is used so that new and old versions of the service can be run simultaneously, + # making rolling upgrades easier + # This is used by SQLAlchemy to explicitly define which fields we're interested + # so it doesn't freak out and say it can't map the structure if other fields are present. + # This could occur from a failed deploy or during an upgrade. + # The other option is to tell SQLAlchemy to ignore differences, but that is ambiguous + # and can interfere with Alembic upgrades. + # + # NOTE: please keep mapper names in alpha-order, easier to track that way + # Exception, id is always first, _fields first + __mapper_args__ = { + 'include_properties': [ + 'id', + 'auth_account_id', + 'created_on', + 'short_name' + ] + } + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + auth_account_id = db.Column('auth_account_id', db.DateTime, nullable=True, index=True) + created_on = db.Column('created_on', db.DateTime, nullable=False, default=datetime.now) + short_name = db.Column('short_name', db.String, nullable=False, index=True) diff --git a/pay-api/src/pay_api/services/eft_service.py b/pay-api/src/pay_api/services/eft_service.py index 384d8a2c6..51b469989 100644 --- a/pay-api/src/pay_api/services/eft_service.py +++ b/pay-api/src/pay_api/services/eft_service.py @@ -16,6 +16,10 @@ from pay_api.utils.enums import PaymentMethod from .deposit_service import DepositService +from .invoice import Invoice +from .invoice_reference import InvoiceReference +from .payment_account import PaymentAccount +from .payment_line_item import PaymentLineItem class EftService(DepositService): @@ -24,3 +28,8 @@ class EftService(DepositService): def get_payment_method_code(self): """Return EFT as the system code.""" return PaymentMethod.EFT.value + + def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLineItem], invoice: Invoice, + **kwargs) -> InvoiceReference: + """Return a static invoice number for direct pay.""" + # Do nothing here as the invoice references will be created later for eft payment reconciliations (TDI17). diff --git a/pay-api/src/pay_api/services/flags.py b/pay-api/src/pay_api/services/flags.py index 97f9d6454..8ac60f45b 100644 --- a/pay-api/src/pay_api/services/flags.py +++ b/pay-api/src/pay_api/services/flags.py @@ -16,6 +16,7 @@ from flask import current_app from ldclient import get as ldclient_get, set_config as ldclient_set_config # noqa: I001 from ldclient.config import Config # noqa: I005 +from ldclient import Context from ldclient.integrations import Files from pay_api.utils import user_context @@ -76,18 +77,13 @@ def _get_client(self): @staticmethod def _get_anonymous_user(): - return { - 'key': 'anonymous' - } + return Context.create('anonymous') @staticmethod def _user_as_key(user: user_context): - user_json = { - 'key': user.sub, - 'userName': user.user_name, - 'firstName': user.first_name - } - return user_json + return Context.builder(user.sub)\ + .set('userName', user.user_name)\ + .set('firstName', user.first_name).build() def is_on(self, flag: str, default: bool = False, user: user_context = None) -> bool: """Assert that the flag is set for this user.""" diff --git a/pay-api/src/pay_api/services/payment_service.py b/pay-api/src/pay_api/services/payment_service.py index 4c042f450..6da906950 100644 --- a/pay-api/src/pay_api/services/payment_service.py +++ b/pay-api/src/pay_api/services/payment_service.py @@ -27,6 +27,7 @@ from .base_payment_system import PaymentSystemService from .fee_schedule import FeeSchedule +from .flags import flags from .invoice import Invoice from .invoice_reference import InvoiceReference from .payment import Payment @@ -64,6 +65,10 @@ def create_invoice(cls, payment_request: Tuple[Dict[str, Any]], authorization: T payment_account = cls._find_payment_account(authorization) payment_method = _get_payment_method(payment_request, payment_account) + + if payment_method == PaymentMethod.EFT.value and not flags.is_on('enable-eft-payment-method', default=False): + raise BusinessException(Error.INVALID_PAYMENT_METHOD) + current_app.logger.info(f'Creating Payment Request : ' f'{payment_method}, {corp_type}, {business_identifier}, ' f'{payment_account.auth_account_id}') diff --git a/pay-api/src/pay_api/utils/errors.py b/pay-api/src/pay_api/utils/errors.py index b1e982bcb..97ee96783 100644 --- a/pay-api/src/pay_api/utils/errors.py +++ b/pay-api/src/pay_api/utils/errors.py @@ -23,6 +23,8 @@ class Error(Enum): INVALID_PAYMENT_ID = 'INVALID_PAYMENT_ID', HTTPStatus.BAD_REQUEST + INVALID_PAYMENT_METHOD = 'INVALID_PAYMENT_METHOD', HTTPStatus.BAD_REQUEST + INVALID_TRANSACTION = 'INVALID_TRANSACTION', HTTPStatus.BAD_REQUEST INVALID_REDIRECT_URI = 'INVALID_REDIRECT_URI', HTTPStatus.BAD_REQUEST diff --git a/pay-api/src/pay_api/version.py b/pay-api/src/pay_api/version.py index 9a136ba93..524aec217 100644 --- a/pay-api/src/pay_api/version.py +++ b/pay-api/src/pay_api/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '1.20.1' # pylint: disable=invalid-name +__version__ = '1.20.2' # pylint: disable=invalid-name diff --git a/pay-api/tests/unit/models/test_eft_short_names.py b/pay-api/tests/unit/models/test_eft_short_names.py new file mode 100644 index 000000000..15658a220 --- /dev/null +++ b/pay-api/tests/unit/models/test_eft_short_names.py @@ -0,0 +1,48 @@ +# Copyright © 2023 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 to assure the EFT File model. + +Test-Suite to ensure that the EFT File model is working as expected. +""" +from datetime import datetime + +from pay_api.models.eft_short_names import EFTShortnames as EFTShortnamesModel + + +def test_eft_short_name_defaults(session): + """Assert eft short names defaults are stored.""" + eft_short_name = EFTShortnamesModel() + eft_short_name.short_name = 'ABC' + eft_short_name.save() + + assert eft_short_name.id is not None + assert eft_short_name.short_name == 'ABC' + assert eft_short_name.created_on.date() == datetime.now().date() + assert eft_short_name.auth_account_id is None + + +def test_eft_short_names_all_attributes(session): + """Assert all eft short names attributes are stored.""" + eft_short_name = EFTShortnamesModel() + eft_short_name.short_name = 'ABC' + eft_short_name.auth_account_id = '1234' + eft_short_name.save() + + assert eft_short_name.id is not None + + eft_short_name = EFTShortnamesModel.find_by_id(eft_short_name.id) + assert eft_short_name.short_name == 'ABC' + assert eft_short_name.auth_account_id == '1234' + assert eft_short_name.created_on.date() == datetime.now().date() diff --git a/pay-api/tests/unit/services/test_payment_service.py b/pay-api/tests/unit/services/test_payment_service.py index d1d6585ec..90e4a0667 100644 --- a/pay-api/tests/unit/services/test_payment_service.py +++ b/pay-api/tests/unit/services/test_payment_service.py @@ -27,6 +27,8 @@ from pay_api.services.payment_service import PaymentService from pay_api.utils.enums import InvoiceStatus, PaymentMethod, PaymentStatus, RoutingSlipStatus from requests.exceptions import ConnectionError, ConnectTimeout, HTTPError + +from pay_api.utils.errors import Error from tests.utilities.base_test import ( factory_invoice, factory_invoice_reference, factory_payment, factory_payment_account, factory_payment_line_item, factory_payment_transaction, factory_routing_slip, get_auth_basic_user, get_auth_premium_user, get_auth_staff, @@ -330,6 +332,21 @@ def test_create_eft_payment(session, public_user_mock): assert payment_response.get('status_code') == 'CREATED' +def test_create_eft_payment_ff_disabled(session, public_user_mock): + """Assert that the payment method EFT feature flag properly disables record creation.""" + factory_payment_account(payment_method_code=PaymentMethod.EFT.value).save() + + with patch('pay_api.services.payment_service.flags.is_on', return_value=False): + with pytest.raises(BusinessException) as exception: + PaymentService.create_invoice( + get_payment_request_with_service_fees( + business_identifier='CP0002000'), + get_auth_premium_user()) + + assert exception is not None + assert exception.value.code == Error.INVALID_PAYMENT_METHOD.name + + def test_create_wire_payment(session, public_user_mock): """Assert that the payment records are created.""" factory_payment_account(payment_method_code=PaymentMethod.WIRE.value).save()