Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14618 - PAY-API changes for routing slip corrections - Part 1 #1063

Merged
merged 6 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pay-api/migrations/versions/fd5c02bb9e01_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Migration to set cas_version_suffix to 1 for existing records

Revision ID: fd5c02bb9e01
Revises: f497d603602e
Create Date: 2022-12-15 13:12:09.563178

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'fd5c02bb9e01'
down_revision = 'f497d603602e'
branch_labels = None
depends_on = None


def upgrade():
op.execute('update routing_slips set cas_version_suffix = 1 where cas_version_suffix is null')
pass
8 changes: 8 additions & 0 deletions pay-api/src/pay_api/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ def find_payment_for_invoice(cls, invoice_id: int):

return query.one_or_none()

@classmethod
def find_payments_for_routing_slip(cls, routing_slip: str):
"""Find payment records created for a routing slip."""
return db.session.query(Payment) \
.filter(Payment.receipt_number == routing_slip) \
.filter(Payment.is_routing_slip.is_(True)) \
.all()

@classmethod
def search_account_payments(cls, auth_account_id: str, payment_status: str, page: int, limit: int):
"""Search payment records created for the account."""
Expand Down
9 changes: 9 additions & 0 deletions pay-api/src/pay_api/models/routing_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ class RoutingSlip(Audit): # pylint: disable=too-many-instance-attributes

parent = relationship('RoutingSlip', remote_side=[number], lazy='select')

def generate_cas_receipt_number(self) -> str:
"""Return a unique identifier - receipt number for CAS."""
receipt_number: str = self.number
if self.parent_number:
receipt_number += 'L'
if self.cas_version_suffix > 1:
receipt_number += f'R{self.cas_version_suffix}'
return receipt_number

@classmethod
def find_by_number(cls, number: str) -> RoutingSlip:
"""Return a routing slip by number."""
Expand Down
36 changes: 35 additions & 1 deletion pay-api/src/pay_api/services/fas/routing_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import RoutingSlip as RoutingSlipModel
from pay_api.models import RoutingSlipSchema
from pay_api.models import Comment as CommentModel
from pay_api.services.fas.routing_slip_status_transition_service import RoutingSlipStatusTransitionService
from pay_api.services.oauth_service import OAuthService
from pay_api.utils.enums import (
Expand Down Expand Up @@ -393,14 +394,14 @@ def update(cls, rs_number: str, action: str, request_json: Dict[str, any], **kwa
raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

if patch_action == PatchActions.UPDATE_STATUS:
# Update the remaining amount as negative total of sum of all totals for that routing slip.
status = request_json.get('status')
RoutingSlipStatusTransitionService.validate_possible_transitions(routing_slip, status)
status = RoutingSlipStatusTransitionService.get_actual_status(status)

RoutingSlip._check_roles_for_status_update(status, user)
# Our routing_slips job will create an invoice (under transactions in the UI).
if status == RoutingSlipStatus.NSF.value:
# Update the remaining amount as negative total of sum of all totals for that routing slip.
total_paid_to_reverse: float = 0
for rs in (routing_slip, *RoutingSlipModel.find_children(routing_slip.number)):
total_paid_to_reverse += rs.total
Expand All @@ -409,12 +410,45 @@ def update(cls, rs_number: str, action: str, request_json: Dict[str, any], **kwa
if routing_slip.invoices:
raise BusinessException(Error.RS_HAS_TRANSACTIONS)
routing_slip.remaining_amount = 0
# This is outside the normal flow of payments, thus why we've done it here in FAS.
elif status == RoutingSlipStatus.CORRECTION.value:
if not request_json.get('payments'):
raise BusinessException(Error.INVALID_REQUEST)
correction_total, comment = cls._calculate_correction_and_comment(rs_number, request_json)
routing_slip.total += correction_total
routing_slip.remaining_amount += correction_total
CommentModel(comment=comment, routing_slip_number=rs_number).flush()

routing_slip.status = status

routing_slip.save()
return cls.find_by_number(rs_number)

@classmethod
def _calculate_correction_and_comment(cls, rs_number: str, request_json: Dict[str, any]):
correction_total = Decimal('0')
comment: str = ''
payments = PaymentModel.find_payments_for_routing_slip(rs_number)
for payment_request in request_json.get('payments'):
if (payment := next(x for x in payments if x.id == payment_request.get('id'))):
paid_amount = payment_request.get('paidAmount', 0)
correction_total += paid_amount - payment.paid_amount
if payment.payment_method_code == PaymentMethod.CASH.value:
comment += f'Cash Payment corrected amount' \
f' ${payment.paid_amount} to ${paid_amount}\n'
else:
comment += f'Cheque Payment {payment.cheque_receipt_number}'
if cheque_receipt_number := payment_request.get('chequeReceiptNumber'):
payment.cheque_receipt_number = cheque_receipt_number
comment += f' cheque receipt number corrected to {cheque_receipt_number}'
if paid_amount != payment.paid_amount:
comment += f' corrected amount ${payment.paid_amount} to ${paid_amount}'
comment += '\n'
payment.paid_amount = paid_amount
payment.paid_usd_amount = payment_request.get('paidUsdAmount', 0)
payment.flush()
return correction_total, comment

@staticmethod
def _check_roles_for_status_update(status: str, user: UserContext):
"""Check roles for the status."""
Expand Down
63 changes: 62 additions & 1 deletion pay-api/tests/unit/api/fas/test_routing_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ def test_routing_slip_status_to_nsf_attempt(client, jwt, app):

def test_routing_slip_void(client, jwt, app):
"""For testing void routing slips."""
# Success, has transactions, has no permissions.
# Create routing slip.
token = jwt.create_jwt(get_claims(roles=[Role.FAS_CREATE.value, Role.FAS_LINK.value,
Role.FAS_SEARCH.value, Role.FAS_EDIT.value]),
token_header)
Expand Down Expand Up @@ -870,3 +870,64 @@ def test_routing_slip_void(client, jwt, app):
data=json.dumps({'status': RoutingSlipStatus.VOID.value}), headers=headers)
assert rv.status_code == 200
assert rv.json.get('remainingAmount') == 0


def test_routing_slip_correction(client, jwt, app):
"""For testing correction of routing slips."""
# Create routing slip.
token = jwt.create_jwt(get_claims(roles=[Role.FAS_CREATE.value, Role.FAS_LINK.value,
Role.FAS_SEARCH.value, Role.FAS_EDIT.value]),
token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}
rs = get_routing_slip_request('438607657')
rv = client.post('/api/v1/fas/routing-slips', data=json.dumps(rs), headers=headers)
payment_id = rv.json.get('payments')[0].get('id')
assert payment_id

# Create invoice.
invoice = factory_invoice(PaymentAccount(id=rv.json.get('paymentAccount').get('id')), folio_number='test_folio',
routing_slip=rv.json.get('number'),
payment_method_code=PaymentMethod.INTERNAL.value)
invoice.save()

# Failure case, no permissions
rv = client.patch(f"/api/v1/fas/routing-slips/{rs.get('number')}?action={PatchActions.UPDATE_STATUS.value}",
data=json.dumps({'status': RoutingSlipStatus.CORRECTION.value}), headers=headers)
assert rv.status_code == 403

token = jwt.create_jwt(get_claims(roles=[Role.FAS_VIEW.value, Role.FAS_EDIT.value, Role.FAS_CORRECTION.value]),
token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

# Failure case, no payments.
rv = client.patch(f"/api/v1/fas/routing-slips/{rs.get('number')}?action={PatchActions.UPDATE_STATUS.value}",
data=json.dumps({'status': RoutingSlipStatus.CORRECTION.value}), headers=headers)
assert rv.status_code == 400

# Success case
payload = {
'status': RoutingSlipStatus.CORRECTION.value,
'payments': [
{
'id': payment_id,
'paidAmount': 50,
'paidUsdAmount': 50,
'chequeReceiptNumber': '911'
}
]
}

rv = client.patch(f"/api/v1/fas/routing-slips/{rs.get('number')}?action={PatchActions.UPDATE_STATUS.value}",
data=json.dumps(payload), headers=headers)
assert rv.status_code == 200
assert rv.json.get('total') == 50
assert rv.json.get('remainingAmount') == 50
assert rv.json.get('status') == RoutingSlipStatus.CORRECTION.value
assert rv.json.get('payments')
assert rv.json.get('payments')[0].get('paidAmount') == 50
assert rv.json.get('payments')[0].get('paidUsdAmount') == 50
assert rv.json.get('payments')[0].get('chequeReceiptNumber') == '911'

rv = client.get(f"/api/v1/fas/routing-slips/{rs.get('number')}/comments", headers=headers)
assert rv.status_code == 200
assert rv.json.get('comments')