diff --git a/pay-api/src/pay_api/services/base_payment_system.py b/pay-api/src/pay_api/services/base_payment_system.py index 647c5c7f2..09100a322 100644 --- a/pay-api/src/pay_api/services/base_payment_system.py +++ b/pay-api/src/pay_api/services/base_payment_system.py @@ -13,6 +13,7 @@ # limitations under the License. """Abstract class for payment system implementation.""" +import functools from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Dict, List @@ -34,9 +35,11 @@ from pay_api.services.payment import Payment from pay_api.services.payment_account import PaymentAccount from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, TransactionStatus +from pay_api.utils.user_context import UserContext from pay_api.utils.util import get_local_formatted_date_time, get_pay_subject_name from .payment_line_item import PaymentLineItem +from .receipt import Receipt class PaymentSystemService(ABC): # pylint: disable=too-many-instance-attributes @@ -182,6 +185,8 @@ def _publish_refund_to_mailer(invoice: InvoiceModel): payment_transaction: PaymentTransactionModel = PaymentTransactionModel.find_recent_completed_by_invoice_id( invoice_id=invoice.id) message_type: str = f'bc.registry.payment.{invoice.payment_method_code.lower()}.refundRequest' + transaction_date_time = receipt.receipt_date if invoice.payment_method_code == PaymentMethod.DRAWDOWN.value \ + else payment_transaction.transaction_end_time filing_description = '' for line_item in invoice.payment_line_items: if filing_description: @@ -196,7 +201,7 @@ def _publish_refund_to_mailer(invoice: InvoiceModel): data=dict( identifier=invoice.business_identifier, orderNumber=receipt.receipt_number, - transactionDateTime=get_local_formatted_date_time(payment_transaction.transaction_end_time), + transactionDateTime=get_local_formatted_date_time(transaction_date_time), transactionAmount=receipt.receipt_amount, transactionId=invoice_ref.invoice_number, refundDate=get_local_formatted_date_time(datetime.now(), '%Y%m%d'), @@ -212,3 +217,56 @@ def _publish_refund_to_mailer(invoice: InvoiceModel): current_app.logger.debug(q_payload) publish_response(payload=q_payload, client_name=current_app.config.get('NATS_MAILER_CLIENT_NAME'), subject=current_app.config.get('NATS_MAILER_SUBJECT')) + + def complete_payment(self, invoice, invoice_reference): + """Create payment and related records as if the payment is complete.""" + Payment.create(payment_method=self.get_payment_method_code(), + payment_system=self.get_payment_system_code(), + payment_status=PaymentStatus.COMPLETED.value, + invoice_number=invoice_reference.invoice_number, + invoice_amount=invoice.total, + payment_account_id=invoice.payment_account_id) + invoice.invoice_status_code = InvoiceStatus.PAID.value + invoice.paid = invoice.total + invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value + # Create receipt. + receipt = Receipt() + receipt.receipt_number = invoice_reference.invoice_number + receipt.receipt_amount = invoice.total + receipt.invoice_id = invoice.id + receipt.receipt_date = datetime.now() + receipt.save() + + +def skip_invoice_for_sandbox(function): + """Skip downstream system (BCOL, CFS) if the invoice creation is in sandbox environment.""" + + @functools.wraps(function) + def wrapper(*func_args, **func_kwargs): + """Complete any post invoice activities if needed.""" + user: UserContext = func_kwargs['user'] + if user.is_sandbox(): + current_app.logger.info('Skipping invoice creation as sandbox token is detected.') + invoice: Invoice = func_args[3] # 3 is invoice from the create_invoice signature + return InvoiceReference.create(invoice.id, f'SANDBOX-{invoice.id}', f'REF-{invoice.id}') + return function(*func_args, **func_kwargs) + + return wrapper + + +def skip_complete_post_invoice_for_sandbox(function): + """Skip actual implementation invocation and mark all records as complete if it's sandbox.""" + + @functools.wraps(function) + def wrapper(*func_args, **func_kwargs): + """Complete any post invoice activities.""" + user: UserContext = func_kwargs['user'] + if user.is_sandbox(): + current_app.logger.info('Completing the payment as sandbox token is detected.') + instance: PaymentSystemService = func_args[0] + instance.complete_payment(func_args[1], func_args[2]) # invoice and invoice ref + instance._release_payment(func_args[1]) # pylint: disable=protected-access + return None + return function(*func_args, **func_kwargs) + + return wrapper diff --git a/pay-api/src/pay_api/services/bcol_service.py b/pay-api/src/pay_api/services/bcol_service.py index 19d1e4964..a360033ba 100644 --- a/pay-api/src/pay_api/services/bcol_service.py +++ b/pay-api/src/pay_api/services/bcol_service.py @@ -20,6 +20,8 @@ from requests.exceptions import HTTPError from pay_api.exceptions import BusinessException, Error +from pay_api.models import Invoice as InvoiceModel +from pay_api.models import Payment as PaymentModel from pay_api.models.corp_type import CorpType from pay_api.utils.enums import AuthHeaderType, ContentType, PaymentMethod, PaymentStatus from pay_api.utils.enums import PaymentSystem as PaySystemCode @@ -27,9 +29,7 @@ from pay_api.utils.user_context import UserContext, user_context from pay_api.utils.util import generate_transaction_number -from pay_api.models import Invoice as InvoiceModel -from pay_api.models import Payment as PaymentModel -from .base_payment_system import PaymentSystemService +from .base_payment_system import PaymentSystemService, skip_complete_post_invoice_for_sandbox, skip_invoice_for_sandbox from .invoice import Invoice from .invoice_reference import InvoiceReference from .oauth_service import OAuthService @@ -45,6 +45,7 @@ def get_payment_system_code(self): return PaySystemCode.BCOL.value @user_context + @skip_invoice_for_sandbox def create_invoice(self, payment_account: PaymentAccount, # pylint: disable=too-many-locals line_items: [PaymentLineItem], invoice: Invoice, **kwargs) -> InvoiceReference: """Create Invoice in PayBC.""" @@ -117,29 +118,16 @@ def get_payment_method_code(self): def process_cfs_refund(self, invoice: InvoiceModel): """Process refund in CFS.""" - super()._publish_refund_to_mailer(invoice) + self._publish_refund_to_mailer(invoice) payment: PaymentModel = PaymentModel.find_payment_for_invoice(invoice.id) payment.payment_status_code = PaymentStatus.REFUNDED.value payment.flush() - def complete_post_invoice(self, invoice: Invoice, invoice_reference: InvoiceReference) -> None: + @user_context + @skip_complete_post_invoice_for_sandbox + def complete_post_invoice(self, invoice: Invoice, # pylint: disable=unused-argument + invoice_reference: InvoiceReference, **kwargs) -> None: """Complete any post payment activities if needed.""" - # pylint: disable=cyclic-import,import-outside-toplevel - from .payment import Payment - from .payment_transaction import PaymentTransaction - - # Create a payment record - Payment.create(payment_method=self.get_payment_method_code(), - payment_system=self.get_payment_system_code(), - payment_status=self.get_default_payment_status(), - invoice_number=invoice_reference.invoice_number, - invoice_amount=invoice.total, - payment_account_id=invoice.payment_account_id) - transaction: PaymentTransaction = PaymentTransaction.create_transaction_for_invoice( - invoice.id, - { - 'clientSystemUrl': '', - 'payReturnUrl': '' - } - ) - transaction.update_transaction(transaction.id, pay_response_url=None) + self.complete_payment(invoice, invoice_reference) + # Publish message to the queue with payment token, so that they can release records on their side. + self._release_payment(invoice=invoice) diff --git a/pay-api/src/pay_api/services/ejv_pay_service.py b/pay-api/src/pay_api/services/ejv_pay_service.py index 55b56bb72..af1a8e9f5 100644 --- a/pay-api/src/pay_api/services/ejv_pay_service.py +++ b/pay-api/src/pay_api/services/ejv_pay_service.py @@ -16,17 +16,14 @@ There are conditions where the payment will be handled for government accounts. """ -from datetime import datetime - from flask import current_app from pay_api.services.base_payment_system import PaymentSystemService from pay_api.services.invoice import Invoice from pay_api.services.invoice_reference import InvoiceReference from pay_api.services.payment_account import PaymentAccount -from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus, PaymentSystem +from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentSystem from pay_api.utils.util import generate_transaction_number - from .oauth_service import OAuthService from .payment_line_item import PaymentLineItem @@ -59,31 +56,9 @@ def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLi def complete_post_invoice(self, invoice: Invoice, invoice_reference: InvoiceReference) -> None: """Complete any post invoice activities if needed.""" - # pylint: disable=import-outside-toplevel, cyclic-import - from .payment import Payment - from .receipt import Receipt - if invoice_reference and invoice_reference.status_code == InvoiceReferenceStatus.ACTIVE.value: # Create a payment record - Payment.create(payment_method=self.get_payment_method_code(), - payment_system=self.get_payment_system_code(), - payment_status=PaymentStatus.COMPLETED.value, - invoice_number=invoice_reference.invoice_number, - invoice_amount=invoice.total, - payment_account_id=invoice.payment_account_id) - invoice.invoice_status_code = InvoiceStatus.PAID.value - invoice.paid = invoice.total - invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value - # Create receipt. - receipt = Receipt() - receipt.receipt_number = invoice_reference.invoice_number - receipt.receipt_amount = invoice.total - receipt.invoice_id = invoice.id - receipt.receipt_date = datetime.now() - - invoice_reference.flush() - receipt.flush() - invoice.save() + self.complete_payment(invoice, invoice_reference) # Publish message to the queue with payment token, so that they can release records on their side. self._release_payment(invoice=invoice) diff --git a/pay-api/src/pay_api/services/internal_pay_service.py b/pay-api/src/pay_api/services/internal_pay_service.py index e201ae526..1a0fc10d3 100644 --- a/pay-api/src/pay_api/services/internal_pay_service.py +++ b/pay-api/src/pay_api/services/internal_pay_service.py @@ -97,27 +97,9 @@ def get_payment_method_code(self): def complete_post_invoice(self, invoice: Invoice, invoice_reference: InvoiceReference) -> None: """Complete any post invoice activities if needed.""" - # pylint: disable=import-outside-toplevel, cyclic-import - from .payment import Payment - from .payment_transaction import PaymentTransaction - - # Create a payment record - current_app.logger.debug('Created payment record') - Payment.create(payment_method=self.get_payment_method_code(), - payment_system=self.get_payment_system_code(), - payment_status=self.get_default_payment_status(), - invoice_number=invoice_reference.invoice_number, - invoice_amount=invoice.total, - payment_account_id=invoice.payment_account_id) - - transaction: PaymentTransaction = PaymentTransaction.create_transaction_for_invoice( - invoice.id, - { - 'clientSystemUrl': '', - 'payReturnUrl': '' - } - ) - transaction.update_transaction(transaction.id, pay_response_url=None) + self.complete_payment(invoice, invoice_reference) + # Publish message to the queue with payment token, so that they can release records on their side. + self._release_payment(invoice=invoice) def process_cfs_refund(self, invoice: InvoiceModel): """Process refund in CFS.""" diff --git a/pay-api/src/pay_api/services/pad_service.py b/pay-api/src/pay_api/services/pad_service.py index b1a4805fb..5bb937fda 100644 --- a/pay-api/src/pay_api/services/pad_service.py +++ b/pay-api/src/pay_api/services/pad_service.py @@ -24,7 +24,9 @@ from pay_api.services.invoice_reference import InvoiceReference from pay_api.services.payment_account import PaymentAccount from pay_api.utils.enums import CfsAccountStatus, InvoiceStatus, PaymentMethod, PaymentSystem +from pay_api.utils.user_context import user_context +from .base_payment_system import skip_complete_post_invoice_for_sandbox, skip_invoice_for_sandbox from .payment_line_item import PaymentLineItem @@ -91,8 +93,10 @@ def update_account(self, name: str, cfs_account: CfsAccountModel, payment_info: updated_cfs_account.flush() return cfs_account + @user_context + @skip_invoice_for_sandbox def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLineItem], invoice: Invoice, - **kwargs) -> InvoiceReference: + **kwargs) -> InvoiceReference: # pylint: disable=unused-argument """Return a static invoice number for direct pay.""" current_app.logger.debug(' None: + @user_context + @skip_complete_post_invoice_for_sandbox + def complete_post_invoice(self, invoice: Invoice, # pylint: disable=unused-argument + invoice_reference: InvoiceReference, **kwargs) -> None: """Complete any post invoice activities if needed.""" # Publish message to the queue with payment token, so that they can release records on their side. self._release_payment(invoice=invoice) def process_cfs_refund(self, invoice: InvoiceModel): """Process refund in CFS.""" - super()._refund_and_create_credit_memo(invoice) + self._refund_and_create_credit_memo(invoice) diff --git a/pay-api/src/pay_api/utils/enums.py b/pay-api/src/pay_api/utils/enums.py index 81e90e9ad..98a218c13 100644 --- a/pay-api/src/pay_api/utils/enums.py +++ b/pay-api/src/pay_api/utils/enums.py @@ -129,6 +129,7 @@ class Role(Enum): FAS_CREATE = 'fas_create' FAS_LINK = 'fas_link' FAS_REFUND_APPROVER = 'fas_refund_approver' + SANDBOX = 'sandbox' class Code(Enum): diff --git a/pay-api/src/pay_api/utils/user_context.py b/pay-api/src/pay_api/utils/user_context.py index 9c64ff9cf..2719e323c 100644 --- a/pay-api/src/pay_api/utils/user_context.py +++ b/pay-api/src/pay_api/utils/user_context.py @@ -113,6 +113,10 @@ def is_system(self) -> bool: """Return True if the user is system user.""" return Role.SYSTEM.value in self._roles if self._roles else False + def is_sandbox(self) -> bool: + """Return True if the user token has sandbox role.""" + return Role.SANDBOX.value in self._roles if self._roles else False + @property def name(self) -> str: """Return the name.""" diff --git a/pay-api/tests/unit/api/test_payment_request.py b/pay-api/tests/unit/api/test_payment_request.py index cce71a4c1..de6c558a3 100755 --- a/pay-api/tests/unit/api/test_payment_request.py +++ b/pay-api/tests/unit/api/test_payment_request.py @@ -33,8 +33,8 @@ activate_pad_account, fake, get_basic_account_payload, get_claims, get_gov_account_payload, get_payment_request, get_payment_request_for_wills, get_payment_request_with_folio_number, get_payment_request_with_no_contact_info, get_payment_request_with_payment_method, get_payment_request_with_service_fees, get_payment_request_without_bn, - get_routing_slip_request, get_unlinked_pad_account_payload, get_waive_fees_payment_request, - get_zero_dollar_payment_request, token_header) + get_premium_account_payload, get_routing_slip_request, get_unlinked_pad_account_payload, + get_waive_fees_payment_request, get_zero_dollar_payment_request, token_header) def test_payment_request_creation(session, client, jwt, app): @@ -1001,3 +1001,25 @@ def test_create_ejv_payment_request_non_billable_account(session, client, jwt, a assert rv.json.get('paymentMethod') == PaymentMethod.EJV.value assert rv.json.get('statusCode') == 'COMPLETED' assert rv.json.get('total') == rv.json.get('paid') + + +@pytest.mark.parametrize('account_payload, pay_method', [ + (get_unlinked_pad_account_payload(account_id=1234), PaymentMethod.PAD.value), + (get_premium_account_payload(account_id=1234), PaymentMethod.DRAWDOWN.value)]) +def test_create_sandbox_payment_requests(session, client, jwt, app, account_payload, pay_method): + """Assert payment request works for PAD accounts.""" + token = jwt.create_jwt(get_claims(roles=[Role.SYSTEM.value, Role.CREATE_SANDBOX_ACCOUNT.value]), token_header) + headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'} + # Create account first + rv = client.post('/api/v1/accounts?sandbox=true', data=json.dumps(account_payload), headers=headers) + + auth_account_id = rv.json.get('accountId') + + token = jwt.create_jwt(get_claims(roles=[Role.SANDBOX.value]), token_header) + headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json', 'Account-Id': auth_account_id} + + payload = get_payment_request() + rv = client.post('/api/v1/payment-requests', data=json.dumps(payload), headers=headers) + + assert rv.json.get('paymentMethod') == pay_method + assert rv.json.get('statusCode') == 'COMPLETED'