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

18439 - EFT Short name mapping endpoints support #1323

Merged
merged 3 commits into from
Nov 9, 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
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)
seeker25 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading