Skip to content

Commit

Permalink
New NSF template, adding search model, migration script, cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigo-barraza committed Dec 7, 2023
1 parent e7cc660 commit b7c6a93
Show file tree
Hide file tree
Showing 7 changed files with 675 additions and 64 deletions.
30 changes: 30 additions & 0 deletions pay-api/migrations/versions/2023_12_05_b65365f7852b_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""empty message
Revision ID: b65365f7852b
Revises: 49eaec3210e0
Create Date: 2023-12-05 12:28:27.025012
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'b65365f7852b'
down_revision = '49eaec3210e0'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('non_sufficient_funds',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('description', sa.String(length=50), nullable=True),
sa.Column('invoice_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
sa.PrimaryKeyConstraint('id')
)


def downgrade():
op.drop_table('non_sufficient_funds')
20 changes: 19 additions & 1 deletion pay-api/src/pay_api/models/non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Model to handle all operations related to Non-Sufficient Funds."""
from __future__ import annotations

from attrs import define
from marshmallow import fields
from sqlalchemy import ForeignKey

Expand Down Expand Up @@ -46,7 +47,7 @@ class NonSufficientFundsModel(BaseModel): # pylint: disable=too-many-instance-a

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
description = db.Column(db.String(50), nullable=True)
invoice_id = db.Column(db.Integer, ForeignKey('invoice.id'), nullable=False)
invoice_id = db.Column(db.Integer, ForeignKey('invoices.id'), nullable=False)


class NonSufficientFundsSchema(BaseSchema): # pylint: disable=too-many-ancestors
Expand All @@ -59,3 +60,20 @@ class Meta(BaseSchema.Meta): # pylint: disable=too-few-public-methods

description = fields.String(data_key='description')
invoice_id = fields.Integer(data_key='invoice_id')


@define
class NonSufficientFundsSearchModel:
"""Used to search for NSF records."""

id: int
invoice_id: int
description: str

@classmethod
def from_row(cls, row: NonSufficientFundsModel):
"""From row is used so we don't tightly couple to our database class.
https://www.attrs.org/en/stable/init.html
"""
return cls(invoice_id=row.invoice_id, description=row.description)
2 changes: 2 additions & 0 deletions pay-api/src/pay_api/resources/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .invoice_receipt import bp as invoice_receipt_bp
from .invoices import bp as invoices_bp
from .meta import bp as meta_bp
from .non_sufficient_funds import bp as non_sufficient_funds_bp
from ..ops import bp as ops_bp
from .payment import bp as payment_bp
from .refund import bp as refund_bp
Expand Down Expand Up @@ -66,6 +67,7 @@ def init_app(self, app):
self.app.register_blueprint(invoices_bp)
self.app.register_blueprint(invoice_receipt_bp)
self.app.register_blueprint(meta_bp)
self.app.register_blueprint(non_sufficient_funds_bp)
self.app.register_blueprint(ops_bp)
self.app.register_blueprint(payment_bp)
self.app.register_blueprint(refund_bp)
Expand Down
32 changes: 25 additions & 7 deletions pay-api/src/pay_api/resources/v1/non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
"""Resource for Account Non-Sufficient Funds endpoints."""
from http import HTTPStatus

from flask import Blueprint, current_app, jsonify, request
from flask import Blueprint, Response, current_app, jsonify
from flask_cors import cross_origin

from pay_api.exceptions import BusinessException
from pay_api.services import NonSufficientFundsService
from pay_api.services.auth import check_auth
from pay_api.utils.auth import jwt as _jwt
Expand All @@ -30,18 +31,35 @@


@bp.route('', methods=['GET', 'OPTIONS'])
@cross_origin(origins='*', methods=['GET', 'POST'])
@cross_origin(origins='*', methods=['GET'])
@_tracing.trace()
@_jwt.requires_auth
def get_non_sufficient_funds(account_id: str):
"""Get non sufficient funds."""
current_app.logger.info('<get_non_sufficient_funds')
# Check if user is authorized to perform this action
check_auth(business_identifier=None, account_id=account_id, one_of_roles=[MAKE_PAYMENT, EDIT_ROLE, VIEW_ROLE])
page: int = int(request.args.get('page', '1'))
limit: int = int(request.args.get('limit', '10'))
response, status = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(account_id=account_id,
page=page,
limit=limit), HTTPStatus.OK
response, status = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(
account_id=account_id), HTTPStatus.OK
current_app.logger.debug('>get_non_sufficient_funds')
return jsonify(response), status


@bp.route('/statement', methods=['GET', 'OPTIONS'])
@cross_origin(origins='*', methods=['GET'])
@_tracing.trace()
@_jwt.requires_auth
def get_non_sufficient_funds_statement_pdf(account_id: str):
"""Get non sufficient funds statement pdf."""
current_app.logger.info('<get_non_sufficient_funds_statement_pdf')
try:
pdf, pdf_filename = NonSufficientFundsService.create_non_sufficient_funds_statement_pdf(account_id=account_id)
response = Response(pdf, 201)
response.headers.set('Content-Disposition', 'attachment', filename=f'{pdf_filename}.pdf')
response.headers.set('Content-Type', 'application/pdf')
response.headers.set('Access-Control-Expose-Headers', 'Content-Disposition')
current_app.logger.debug('>get_non_sufficient_funds_statement')
return response
except BusinessException as exception:
current_app.logger.debug('>get_non_sufficient_funds_statement_pdf')
return exception.response()
145 changes: 92 additions & 53 deletions pay-api/src/pay_api/services/non_sufficient_funds.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
from pay_api.models import Payment as PaymentModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import PaymentSchema, db
from pay_api.utils.user_context import user_context
from pay_api.utils.enums import (
AuthHeaderType, ContentType)
from .oauth_service import OAuthService


class NonSufficientFundsService: # pylint: disable=too-many-instance-attributes,too-many-public-methods
Expand Down Expand Up @@ -112,81 +116,116 @@ def populate(value: NonSufficientFundsModel):
return non_sufficient_funds_service

@staticmethod
def query_all_non_sufficient_funds_invoices(account_id: str, page: int, limit: int):
def save_non_sufficient_funds(invoice_id: int, description: str) -> NonSufficientFundsService:
"""Create Non-Sufficient Funds record."""
current_app.logger.debug('<save_non_sufficient_funds')
non_sufficient_funds_service = NonSufficientFundsService()

non_sufficient_funds_service.invoice_id = invoice_id
non_sufficient_funds_service.description = description
non_sufficient_funds_dao = non_sufficient_funds_service.save()

non_sufficient_funds_service = NonSufficientFundsService.populate(non_sufficient_funds_dao)
current_app.logger.debug('>save_non_sufficient_funds')
return non_sufficient_funds_service

@staticmethod
def query_all_non_sufficient_funds_invoices(account_id: str):
"""Return all Non-Sufficient Funds invoices."""
query = db.session.query(PaymentModel, InvoiceModel) \
.join(PaymentAccountModel, PaymentAccountModel.id == PaymentModel.payment_account_id) \
.outerjoin(InvoiceReferenceModel, InvoiceReferenceModel.invoice_number == PaymentModel.invoice_number) \
.outerjoin(InvoiceModel, InvoiceReferenceModel.invoice_id == InvoiceModel.id) \
.join(NonSufficientFundsModel, InvoiceModel.id == NonSufficientFundsModel.invoice_id) \
.filter(PaymentAccountModel.auth_account_id == account_id) \
.filter(PaymentModel.paid_amount > 0)

query = query.order_by(PaymentModel.id.asc())
pagination = query.paginate(per_page=limit, page=page)
results, total = pagination.items, pagination.total
.filter(PaymentModel.paid_amount > 0) \
.order_by(PaymentModel.id.asc())

non_sufficient_funds_invoices = query.all()
results, total = non_sufficient_funds_invoices, len(non_sufficient_funds_invoices)

return results, total

@staticmethod
def find_all_non_sufficient_funds_invoices(account_id: str, page: int, limit: int):
# pylint: disable=too-many-locals
"""Return all Non-Sufficient Funds."""
results, total = NonSufficientFundsService.query_all_non_sufficient_funds_invoices(account_id=account_id,
page=page, limit=limit)

data = {
'total': total,
'page': page,
'limit': limit,
'invoices': [],
'total_amount': 0,
'total_amount_remaining': 0,
def accumulate_totals(results, payment_schema, invoice_schema):
accumulated = {
'last_payment_id': None,
'total_amount_to_pay': 0,
'total_amount_paid': 0,
'total_nsf_amount': 0,
'total_nsf_count': 0
'total_nsf_count': 0,
'invoices': []
}

payment_schema = PaymentSchema()
inv_schema = InvoiceSchema(exclude=('receipts', 'references', '_links'))
last_payment_id = None
total_amount_to_pay = 0
total_amount_paid = 0
total_nsf_amount = 0
total_nsf_count = 0

for payment, invoice in results:
if payment.id != last_payment_id:
total_amount_paid += payment.paid_amount
total_amount_to_pay += invoice.total

if payment.id != accumulated['last_payment_id']:
accumulated['total_amount_paid'] += payment.paid_amount
accumulated['total_amount_to_pay'] += invoice.total
nsf_line_items = [item for item in invoice.line_items if item.description == 'NSF']
total_nsf_count += len(nsf_line_items)
total_nsf_amount += sum(item.total for item in nsf_line_items)

accumulated['total_nsf_count'] += len(nsf_line_items)
accumulated['total_nsf_amount'] += sum(item.total for item in nsf_line_items)
payment_dict = payment_schema.dump(payment)
payment_dict['invoices'] = [inv_schema.dump(invoice)]
data['invoices'].append(payment_dict)
payment_dict['invoices'] = [invoice_schema.dump(invoice)]
accumulated['invoices'].append(payment_dict)
else:
payment_dict['invoices'].append(inv_schema.dump(invoice))
last_payment_id = payment.id
accumulated['invoices'][-1]['invoices'].append(invoice_schema.dump(invoice))
accumulated['last_payment_id'] = payment.id

return accumulated

@staticmethod
def find_all_non_sufficient_funds_invoices(account_id: str):
results, total = NonSufficientFundsService.query_all_non_sufficient_funds_invoices(account_id=account_id)
payment_schema = PaymentSchema()
invoice_schema = InvoiceSchema(exclude=('receipts', 'references', '_links'))
accumulated = NonSufficientFundsService.accumulate_totals(results, payment_schema, invoice_schema)

data['total_amount'] = total_amount_to_pay - total_nsf_amount
data['total_amount_remaining'] = total_amount_to_pay - total_amount_paid
data['total_nsf_amount'] = total_nsf_amount
data['total_nsf_count'] = total_nsf_count
data = {
'total': total,
'invoices': accumulated['invoices'],
'total_amount': accumulated['total_amount_to_pay'] - accumulated['total_nsf_amount'],
'total_amount_remaining': accumulated['total_amount_to_pay'] - accumulated['total_amount_paid'],
'total_nsf_amount': accumulated['total_nsf_amount'],
'total_nsf_count': accumulated['total_nsf_count']
}

return data

@staticmethod
def save_non_sufficient_funds(invoice_id: int, description: str) -> NonSufficientFundsService:
"""Create Non-Sufficient Funds record."""
current_app.logger.debug('<save_non_sufficient_funds')
non_sufficient_funds_service = NonSufficientFundsService()
@user_context
def create_non_sufficient_funds_statement_pdf(account_id: str, **kwargs):
current_app.logger.debug('<generate_non_sufficient_funds_statement_pdf')
invoice = NonSufficientFundsService.find_all_non_sufficient_funds_invoices(account_id=account_id)

template_vars = {
'suspendedOn': '',
'totalAmountRemaining': invoice['total_amount_remaining'],
'totalAmount': invoice['total_amount'],
'totalNfsAmount': invoice['total_nsf_amount'],
'invoices': invoice['invoices'],
'invoiceNumber': '',
'businessName': '',
'account': {
'paymentPreference': {
'bcOnlineAccountId': '123456'
}
},
'statement': {
'id': '123456'
}
}
invoice_pdf_dict = {
'templateName': 'non_sufficient_funds',
'reportName': 'non_sufficient_funds',
'templateVars': template_vars,
'populatePageNumber': True
}
current_app.logger.info('Invoice PDF Dict %s', invoice_pdf_dict)

non_sufficient_funds_service.invoice_id = invoice_id
non_sufficient_funds_service.description = description
non_sufficient_funds_dao = non_sufficient_funds_service.save()
pdf_response = OAuthService.post(current_app.config.get('REPORT_API_BASE_URL'),
kwargs['user'].bearer_token, AuthHeaderType.BEARER,
ContentType.JSON, invoice_pdf_dict)
print('🟢🟢🟢🟢🟢', pdf_response)
current_app.logger.debug('<OAuthService responded to generate_non_sufficient_funds_statement_pdf')

non_sufficient_funds_service = NonSufficientFundsService.populate(non_sufficient_funds_dao)
current_app.logger.debug('>save_non_sufficient_funds')
return non_sufficient_funds_service
return pdf_response, invoice_pdf_dict.get('reportName')
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
from pay_api.models import PaymentLineItem as PaymentLineItemModel
from pay_api.models import Receipt as ReceiptModel
from pay_api.models import db
from pay_api.services.non_sufficient_funds import NonSufficientFundsService
from pay_api.services.cfs_service import CFSService
from pay_api.services.non_sufficient_funds import NonSufficientFundsService
from pay_api.services.payment_transaction import PaymentTransaction as PaymentTransactionService
from pay_api.services.queue_publisher import publish
from pay_api.utils.enums import (
Expand Down Expand Up @@ -481,8 +481,6 @@ def _process_failed_payments(row):
invoice: InvoiceModel = InvoiceModel.find_by_id(identifier=inv_reference.invoice_id)
invoice.invoice_status_code = InvoiceStatus.SETTLEMENT_SCHEDULED.value
invoice.paid = 0
# Add invoice to non sufficient funds table
NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice.id, description='NSF')

# Create an invoice for NSF for this account
invoice = _create_nsf_invoice(cfs_account, inv_number, payment_account)
Expand Down Expand Up @@ -758,6 +756,9 @@ def _create_nsf_invoice(cfs_account: CfsAccountModel, inv_number: str,
)
inv_ref.save()

# Add invoice to non sufficient funds table
NonSufficientFundsService.save_non_sufficient_funds(invoice_id=invoice.id, description='NSF')

return invoice


Expand Down
503 changes: 503 additions & 0 deletions report-api/report-templates/non_sufficient_funds.html

Large diffs are not rendered by default.

0 comments on commit b7c6a93

Please sign in to comment.