Skip to content

Commit

Permalink
18439 - EFT Short name mapping endpoints support (#1323)
Browse files Browse the repository at this point in the history
* EFT Short name to auth account mapping endpoints

* PR Feedback

* lint fixes
  • Loading branch information
ochiu authored Nov 9, 2023
1 parent e54fac0 commit 951df02
Show file tree
Hide file tree
Showing 17 changed files with 729 additions and 20 deletions.
3 changes: 3 additions & 0 deletions pay-api/src/pay_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ class _Config(): # pylint: disable=too-few-public-methods
CFS_RECEIPT_PREFIX = os.getenv('CFS_RECEIPT_PREFIX', 'RCPT')
CFS_PARTY_PREFIX = os.getenv('CFS_PARTY_PREFIX', 'BCR-')

# EFT Config
EFT_INVOICE_PREFIX = os.getenv('EFT_INVOICE_PREFIX', 'REG')

# PAYBC Direct Pay Settings
PAYBC_DIRECT_PAY_REF_NUMBER = _get_config('PAYBC_DIRECT_PAY_REF_NUMBER')
PAYBC_DIRECT_PAY_API_KEY = _get_config('PAYBC_DIRECT_PAY_API_KEY')
Expand Down
2 changes: 1 addition & 1 deletion pay-api/src/pay_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .eft_credit import EFTCredit
from .eft_file import EFTFile
from .eft_process_status_code import EFTProcessStatusCode
from .eft_short_names import EFTShortnames
from .eft_short_names import EFTShortnames, EFTShortnameSchema
from .eft_transaction import EFTTransaction
from .ejv_file import EjvFile
from .ejv_header import EjvHeader
Expand Down
8 changes: 8 additions & 0 deletions pay-api/src/pay_api/models/eft_credit.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ class EFTCredit(BaseModel): # pylint:disable=too-many-instance-attributes
def find_by_payment_account_id(cls, payment_account_id: int):
"""Find EFT Credit by payment account id."""
return cls.query.filter_by(payment_account_id=payment_account_id).all()

@classmethod
def update_account_by_short_name_id(cls, short_name_id: int, payment_account_id: int):
"""Update all payment account ids for short name."""
db.session.query(EFTCredit) \
.filter(EFTCredit.short_name_id == short_name_id) \
.update({EFTCredit.payment_account_id: payment_account_id}, synchronize_session='fetch')
db.session.commit()
37 changes: 37 additions & 0 deletions pay-api/src/pay_api/models/eft_short_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"""Model to handle EFT TDI17 short name to BCROS account mapping."""

from datetime import datetime
from attrs import define


from .base_model import VersionedModel
from .db import db
Expand Down Expand Up @@ -46,3 +48,38 @@ class EFTShortnames(VersionedModel): # pylint: disable=too-many-instance-attrib
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)

@classmethod
def find_by_short_name(cls, short_name: str):
"""Find by eft short name."""
return cls.query.filter_by(short_name=short_name).one_or_none()

@classmethod
def find_all_short_names(cls, include_all: bool, page: int, limit: int):
"""Return eft short names."""
query = db.session.query(EFTShortnames)

if not include_all:
query = query.filter(EFTShortnames.auth_account_id.is_(None))

query = query.order_by(EFTShortnames.short_name.asc())
pagination = query.paginate(per_page=limit, page=page)
return pagination.items, pagination.total


@define
class EFTShortnameSchema: # pylint: disable=too-few-public-methods
"""Main schema used to serialize the EFT Short name."""

id: int
short_name: str
account_id: str
created_on: datetime

@classmethod
def from_row(cls, row: EFTShortnames):
"""From row is used so we don't tightly couple to our database class.
https://www.attrs.org/en/stable/init.html
"""
return cls(id=row.id, short_name=row.short_name, account_id=row.auth_account_id, created_on=row.created_on)
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 @@ -23,6 +23,7 @@
from .bank_accounts import bp as bank_accounts_bp
from .code import bp as code_bp
from .distributions import bp as distributions_bp
from .eft_short_names import bp as eft_short_names_bp
from .fas import fas_refund_bp, fas_routing_slip_bp
from .fee import bp as fee_bp
from .fee_schedule import bp as fee_schedule_bp
Expand Down Expand Up @@ -56,6 +57,7 @@ def init_app(self, app):
self.app.register_blueprint(bank_accounts_bp)
self.app.register_blueprint(code_bp)
self.app.register_blueprint(distributions_bp)
self.app.register_blueprint(eft_short_names_bp)
self.app.register_blueprint(fas_refund_bp)
self.app.register_blueprint(fas_routing_slip_bp)
self.app.register_blueprint(fee_bp)
Expand Down
85 changes: 85 additions & 0 deletions pay-api/src/pay_api/resources/v1/eft_short_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 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.
"""Resource for EFT Short name."""
from http import HTTPStatus

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

from pay_api.exceptions import BusinessException
from pay_api.services.eft_short_names import EFTShortnames as EFTShortnameService
from pay_api.utils.auth import jwt as _jwt
from pay_api.utils.endpoints_enums import EndpointEnum
from pay_api.utils.enums import Role
from pay_api.utils.trace import tracing as _tracing

bp = Blueprint('EFT_SHORT_NAMES', __name__, url_prefix=f'{EndpointEnum.API_V1.value}/eft-shortnames')


@bp.route('', methods=['GET', 'OPTIONS'])
@cross_origin(origins='*', methods=['GET'])
@_jwt.requires_auth
@_jwt.has_one_of_roles([Role.SYSTEM.value, Role.STAFF.value])
def get_eft_shortnames():
"""Get all eft short name records."""
current_app.logger.info('<get_eft_shortnames')

include_all = bool(request.args.get('includeAll', 'false').lower() == 'true')
page: int = int(request.args.get('page', '1'))
limit: int = int(request.args.get('limit', '10'))

response, status = EFTShortnameService.search(include_all, page, limit), HTTPStatus.OK
current_app.logger.debug('>get_eft_shortnames')
return jsonify(response), status


@bp.route('/<int:short_name_id>', methods=['GET', 'OPTIONS'])
@cross_origin(origins='*', methods=['GET', 'PATCH'])
@_tracing.trace()
@_jwt.requires_auth
@_jwt.has_one_of_roles([Role.SYSTEM.value, Role.STAFF.value])
def get_eft_shortname(short_name_id: int):
"""Get EFT short name details."""
current_app.logger.info('<get_eft_shortname')

if not (eft_short_name := EFTShortnameService.find_by_short_name_id(short_name_id)):
response, status = {'message': 'The requested EFT short name could not be found.'}, \
HTTPStatus.NOT_FOUND
else:
response, status = eft_short_name.asdict(), HTTPStatus.OK
current_app.logger.debug('>get_eft_shortname')
return jsonify(response), status


@bp.route('/<int:short_name_id>', methods=['PATCH'])
@cross_origin(origins='*')
@_tracing.trace()
@_jwt.has_one_of_roles([Role.SYSTEM.value, Role.STAFF.value])
def patch_eft_shortname(short_name_id: int):
"""Update EFT short name mapping."""
current_app.logger.info('<patch_eft_shortname')
request_json = request.get_json()

try:
if not EFTShortnameService.find_by_short_name_id(short_name_id):
response, status = {'message': 'The requested EFT short name could not be found.'}, \
HTTPStatus.NOT_FOUND
else:
account_id = request_json.get('accountId', None)
response, status = EFTShortnameService.patch(short_name_id, account_id).asdict(), HTTPStatus.OK
except BusinessException as exception:
return exception.response()

current_app.logger.debug('>patch_eft_shortname')
return jsonify(response), status
27 changes: 27 additions & 0 deletions pay-api/src/pay_api/schemas/schemas/eft_short_name.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://bcrs.gov.bc.ca/.well_known/schemas/eft_short_name",
"type": "object",
"title": "EFT Short name response",
"required": [
"shortName"
],
"properties": {
"shortName": {
"$id": "#/properties/shortName",
"type": "string",
"title": "EFT short name",
"examples": [
"ABC123"
]
},
"accountId": {
"$id": "#/properties/accountId",
"type": ["string", "null"],
"title": "BC Registries Account Number",
"examples": [
"1234"
]
}
}
}
64 changes: 62 additions & 2 deletions pay-api/src/pay_api/services/eft_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service to manage CFS EFT Payments."""
from datetime import datetime

from pay_api.utils.enums import PaymentMethod
from flask import current_app

from pay_api.models import Invoice as InvoiceModel
from pay_api.models import InvoiceReference as InvoiceReferenceModel
from pay_api.models import Payment as PaymentModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import Receipt as ReceiptModel
from pay_api.utils.enums import InvoiceReferenceStatus, InvoiceStatus, PaymentMethod, PaymentStatus

from .deposit_service import DepositService
from .invoice import Invoice
Expand All @@ -35,5 +43,57 @@ def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLi
# Do nothing here as the invoice references will be created later for eft payment reconciliations (TDI17).

def apply_credit(self, invoice: Invoice) -> None:
"""Apply credit to the invoice."""
"""Apply eft credit to the invoice."""
invoice_balance = invoice.total - (invoice.paid or 0) # balance before applying credits
payment_account = PaymentAccount.find_by_id(invoice.payment_account_id)
invoice_model = InvoiceModel.find_by_id(invoice.id)

payment_account.deduct_eft_credit(invoice_model)
new_invoice_balance = invoice.total - (invoice.paid or 0)

payment = self.create_payment(payment_account=payment_account,
invoice=invoice_model,
payment_date=datetime.now(),
paid_amount=invoice_balance - new_invoice_balance).save()

self.create_invoice_reference(invoice=invoice_model, payment=payment).save()
self.create_receipt(invoice=invoice_model, payment=payment).save()
self._release_payment(invoice=invoice)

def create_payment(self, payment_account: PaymentAccountModel, invoice: InvoiceModel, payment_date: datetime,
paid_amount) -> PaymentModel:
"""Create a payment record for an invoice."""
payment = PaymentModel(payment_method_code=self.get_payment_method_code(),
payment_status_code=PaymentStatus.COMPLETED.value,
payment_system_code=self.get_payment_system_code(),
invoice_number=f'{current_app.config["EFT_INVOICE_PREFIX"]}{invoice.id}',
invoice_amount=invoice.total,
payment_account_id=payment_account.id,
payment_date=payment_date,
paid_amount=paid_amount,
receipt_number=invoice.id)
return payment

@staticmethod
def create_invoice_reference(invoice: InvoiceModel, payment: PaymentModel) -> InvoiceReferenceModel:
"""Create an invoice reference record."""
if not (invoice_reference := InvoiceReferenceModel
.find_any_active_reference_by_invoice_number(payment.invoice_number)):
invoice_reference = InvoiceReferenceModel()

invoice_reference.invoice_id = invoice.id
invoice_reference.invoice_number = payment.invoice_number
invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value \
if invoice.invoice_status_code == InvoiceStatus.PAID.value \
else InvoiceReferenceStatus.ACTIVE.value

return invoice_reference

@staticmethod
def create_receipt(invoice: InvoiceModel, payment: PaymentModel) -> ReceiptModel:
"""Create a receipt record for an invoice payment."""
receipt: ReceiptModel = ReceiptModel(receipt_date=payment.payment_date,
receipt_amount=payment.paid_amount,
invoice_id=invoice.id,
receipt_number=payment.receipt_number)
return receipt
Loading

0 comments on commit 951df02

Please sign in to comment.