Skip to content

Commit

Permalink
Changes for marking payment records as complete for sandbox tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
sumesh-aot committed Nov 13, 2021
1 parent 7028ab0 commit 2e8bd84
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 78 deletions.
60 changes: 59 additions & 1 deletion pay-api/src/pay_api/services/base_payment_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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'),
Expand All @@ -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
36 changes: 12 additions & 24 deletions pay-api/src/pay_api/services/bcol_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
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
from pay_api.utils.errors import get_bcol_error
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
Expand All @@ -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."""
Expand Down Expand Up @@ -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)
29 changes: 2 additions & 27 deletions pay-api/src/pay_api/services/ejv_pay_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
24 changes: 3 additions & 21 deletions pay-api/src/pay_api/services/internal_pay_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
13 changes: 10 additions & 3 deletions pay-api/src/pay_api/services/pad_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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('<create_invoice_pad_service')
# Do nothing here as the invoice references are created later.
Expand All @@ -101,11 +105,14 @@ def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLi
payment_account.credit = 0 if account_credit < invoice.total else account_credit - invoice.total
payment_account.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 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)
1 change: 1 addition & 0 deletions pay-api/src/pay_api/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions pay-api/src/pay_api/utils/user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
26 changes: 24 additions & 2 deletions pay-api/tests/unit/api/test_payment_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'

0 comments on commit 2e8bd84

Please sign in to comment.