Skip to content

Commit

Permalink
18468 - EFT Credits / Model updates (#1315)
Browse files Browse the repository at this point in the history
* 17829 - EFT Credits / Model updates

* Remove unused EFT/WIRE tests and methods

* eft model updates for reconciliation handling

* fix eft_transactions model column typo

* fix eft_transaction model mapper

* revert update invoice flow changes - does not apply to EFT

* move deduct eft credits tests to payment account service

* EFT Credit model updates

* PR feedback, fix tests
  • Loading branch information
ochiu authored Nov 6, 2023
1 parent 9a56377 commit 1a1cb0c
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 73 deletions.
16 changes: 0 additions & 16 deletions jobs/payment-jobs/tasks/cfs_create_invoice_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ def create_invoices(cls):
current_app.logger.info('<< Starting Online Banking Invoice Creation')
cls._create_online_banking_invoices()
current_app.logger.info('>> Done Online Banking Invoice Creation')
current_app.logger.info('<< Starting EFT Invoice Creation')
cls._create_eft_invoices()
current_app.logger.info('>> Done EFT Invoice Creation')
current_app.logger.info('<< Starting Wire Invoice Creation')
cls._create_wire_invoices()
current_app.logger.info('>> Done Wire Invoice Creation')

if current_app.config.get('DISABLE_CFS_FAS_INTEGRATION'):
return
Expand Down Expand Up @@ -344,16 +338,6 @@ def _create_online_banking_invoices(cls):
"""Create online banking invoices to CFS system."""
cls._create_single_invoice_per_purchase(PaymentMethod.ONLINE_BANKING)

@classmethod
def _create_eft_invoices(cls):
"""Create EFT invoices to CFS system."""
cls._create_single_invoice_per_purchase(PaymentMethod.EFT)

@classmethod
def _create_wire_invoices(cls):
"""Create Wire invoices to CFS system."""
cls._create_single_invoice_per_purchase(PaymentMethod.WIRE)

@classmethod
def _create_single_invoice_per_purchase(cls, payment_method: PaymentMethod):
"""Create one CFS invoice per purchase."""
Expand Down
53 changes: 2 additions & 51 deletions jobs/payment-jobs/tests/jobs/test_cfs_create_invoice_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
from tasks.cfs_create_invoice_task import CreateInvoiceTask

from .factory import (
factory_create_eft_account, factory_create_online_banking_account, factory_create_pad_account,
factory_create_wire_account, factory_invoice, factory_payment_line_item, factory_routing_slip_account)
factory_create_online_banking_account, factory_create_pad_account, factory_invoice, factory_payment_line_item,
factory_routing_slip_account)


def test_create_invoice(session):
Expand Down Expand Up @@ -272,52 +272,3 @@ def test_create_online_banking_transaction(session):

assert inv_ref
assert updated_invoice.invoice_status_code == InvoiceStatus.SETTLEMENT_SCHEDULED.value


def test_create_eft_transaction(session):
"""Assert EFT invoices are created."""
# Create an account and an invoice for the account
account = factory_create_eft_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value)
previous_day = datetime.now() - timedelta(days=1)
# Create an invoice for this account
invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, payment_method_code=None)

fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN')
line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id)
line.save()

assert invoice.invoice_status_code == InvoiceStatus.CREATED.value

CreateInvoiceTask.create_invoices()

updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id)
inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \
find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value)

assert inv_ref
assert updated_invoice.invoice_status_code == InvoiceStatus.SETTLEMENT_SCHEDULED.value


def test_create_wire_transaction(session):
"""Assert Wire invoices are created."""
# Create an account and an invoice for the account
account = factory_create_wire_account(auth_account_id='1', status=CfsAccountStatus.ACTIVE.value)
previous_day = datetime.now() - timedelta(days=1)
# Create an invoice for this account
invoice = factory_invoice(payment_account=account, created_on=previous_day, total=10, payment_method_code=None)

fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type('CP', 'OTANN')
line = factory_payment_line_item(invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id)
line.save()

assert invoice.invoice_status_code == InvoiceStatus.CREATED.value
assert invoice.payment_method_code == 'WIRE'

CreateInvoiceTask.create_invoices()

updated_invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id)
inv_ref: InvoiceReferenceModel = InvoiceReferenceModel. \
find_by_invoice_id_and_status(invoice.id, InvoiceReferenceStatus.ACTIVE.value)

assert inv_ref
assert updated_invoice.invoice_status_code == InvoiceStatus.SETTLEMENT_SCHEDULED.value
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""17829-eft-credits-shortname-versioning
Revision ID: 598bbfce4dad
Revises: 194cdd7cf986
Create Date: 2023-10-27 12:11:36.931753
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '598bbfce4dad'
down_revision = '2ef58b39cafc'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('eft_credits',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('amount', sa.Numeric(), nullable=False),
sa.Column('remaining_amount', sa.Numeric(), nullable=False),
sa.Column('payment_account_id', sa.Integer(), nullable=True),
sa.Column('eft_file_id', sa.Integer(), nullable=False),
sa.Column('short_name_id', sa.Integer(), nullable=False),
sa.Column('created_on', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['payment_account_id'], ['payment_accounts.id'], ),
sa.ForeignKeyConstraint(['eft_file_id'], ['eft_files.id'], ),
sa.ForeignKeyConstraint(['short_name_id'], ['eft_short_names.id'], ),
sa.PrimaryKeyConstraint('id')
)

op.create_table('eft_short_names_version',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('short_name', sa.String(), nullable=False),
sa.Column('auth_account_id', sa.String(length=50), nullable=True),
sa.Column('created_on', sa.DateTime(), nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('id', 'transaction_id')
)

op.add_column('eft_transactions', sa.Column('short_name_id', sa.Integer(), nullable=True))
op.add_column('eft_transactions', sa.Column('deposit_amount_cents', sa.BigInteger(), nullable=True)),
op.create_foreign_key('eft_transactions_short_name_fk', 'eft_transactions',
'eft_short_names', ['short_name_id'], ['id'])


def downgrade():
op.drop_table('eft_credits')
op.drop_table('eft_short_names_version')
op.drop_constraint('eft_transactions_short_name_fk', 'eft_transactions', type_='foreignkey')
op.drop_column('eft_transactions', 'short_name_id')
op.drop_column('eft_transactions', 'deposit_amount_cents')
2 changes: 2 additions & 0 deletions pay-api/src/pay_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
from .db import db, ma # noqa: I001
from .disbursement_status_code import DisbursementStatusCode
from .distribution_code import DistributionCode, DistributionCodeLink
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_transaction import EFTTransaction
from .ejv_file import EjvFile
from .ejv_header import EjvHeader
Expand Down
61 changes: 61 additions & 0 deletions pay-api/src/pay_api/models/eft_credit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.
"""Model to handle all operations related to EFT Credits data."""
from datetime import datetime
from sqlalchemy import ForeignKey

from .base_model import BaseModel
from .db import db


class EFTCredit(BaseModel): # pylint:disable=too-many-instance-attributes
"""This class manages all of the base data for EFT credits."""

__tablename__ = 'eft_credits'
# this mapper is used so that new and old versions of the service can be run simultaneously,
# making rolling upgrades easier
# This is used by SQLAlchemy to explicitly define which fields we're interested
# so it doesn't freak out and say it can't map the structure if other fields are present.
# This could occur from a failed deploy or during an upgrade.
# The other option is to tell SQLAlchemy to ignore differences, but that is ambiguous
# and can interfere with Alembic upgrades.
#
# NOTE: please keep mapper names in alpha-order, easier to track that way
# Exception, id is always first, _fields first
__mapper_args__ = {
'include_properties': [
'id',
'amount',
'created_on',
'eft_file_id',
'short_name_id',
'payment_account_id',
'remaining_amount'
]
}

id = db.Column(db.Integer, primary_key=True, autoincrement=True)

amount = db.Column(db.Numeric(19, 2), nullable=False)
remaining_amount = db.Column(db.Numeric(19, 2), nullable=False)
created_on = db.Column('created_on', db.DateTime, nullable=True, default=datetime.now)

eft_file_id = db.Column(db.Integer, ForeignKey('eft_files.id'), nullable=False)
short_name_id = db.Column(db.Integer, ForeignKey('eft_short_names.id'), nullable=False)
payment_account_id = db.Column(db.Integer, ForeignKey('payment_accounts.id'), nullable=True, index=True)

@classmethod
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()
4 changes: 2 additions & 2 deletions pay-api/src/pay_api/models/eft_short_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

from datetime import datetime

from .base_model import BaseModel
from .base_model import VersionedModel
from .db import db


class EFTShortnames(BaseModel): # pylint: disable=too-many-instance-attributes
class EFTShortnames(VersionedModel): # pylint: disable=too-many-instance-attributes
"""This class manages the EFT short name to auth account mapping."""

__tablename__ = 'eft_short_names'
Expand Down
4 changes: 4 additions & 0 deletions pay-api/src/pay_api/models/eft_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class EFTTransaction(BaseModel): # pylint: disable=too-many-instance-attributes
'batch_number',
'completed_on',
'created_on',
'deposit_amount_cents',
'error_messages',
'file_id',
'last_updated_on',
Expand All @@ -50,6 +51,7 @@ class EFTTransaction(BaseModel): # pylint: disable=too-many-instance-attributes
'jv_type',
'jv_number',
'sequence_number',
'short_name_id',
'status_code'
]
}
Expand All @@ -66,4 +68,6 @@ class EFTTransaction(BaseModel): # pylint: disable=too-many-instance-attributes
jv_type = db.Column('jv_type', db.String(1), nullable=True)
jv_number = db.Column('jv_number', db.String(10), nullable=True)
sequence_number = db.Column('sequence_number', db.String(3), nullable=True)
short_name_id = db.Column(db.Integer, ForeignKey('eft_short_names.id'), nullable=True)
status_code = db.Column(db.String, ForeignKey('eft_process_status_codes.code'), nullable=False)
deposit_amount_cents = db.Column('deposit_amount_cents', db.BigInteger, nullable=True)
4 changes: 4 additions & 0 deletions pay-api/src/pay_api/services/eft_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ def create_invoice(self, payment_account: PaymentAccount, line_items: [PaymentLi
**kwargs) -> InvoiceReference:
"""Return a static invoice number for direct pay."""
# 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."""
self._release_payment(invoice=invoice)
53 changes: 51 additions & 2 deletions pay-api/src/pay_api/services/payment_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,27 @@

from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple

from flask import current_app
from sentry_sdk import capture_message
from sqlalchemy import func

from pay_api.exceptions import BusinessException, ServiceUnavailableException
from pay_api.models import AccountFee as AccountFeeModel
from pay_api.models import AccountFeeSchema
from pay_api.models import CfsAccount as CfsAccountModel
from pay_api.models import EFTCredit as EFTCreditModel
from pay_api.models import Invoice as InvoiceModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models import PaymentAccountSchema
from pay_api.models import StatementSettings as StatementSettingsModel
from pay_api.models import db
from pay_api.services.cfs_service import CFSService
from pay_api.services.distribution_code import DistributionCode
from pay_api.services.queue_publisher import publish_response
from pay_api.utils.enums import CfsAccountStatus, MessageType, PaymentMethod, PaymentSystem, StatementFrequency
from pay_api.utils.enums import (
CfsAccountStatus, InvoiceStatus, MessageType, PaymentMethod, PaymentSystem, StatementFrequency)
from pay_api.utils.errors import Error
from pay_api.utils.user_context import UserContext, user_context
from pay_api.utils.util import (
Expand Down Expand Up @@ -556,6 +560,51 @@ def find_by_id(cls, account_id: int):
account._dao = PaymentAccountModel.find_by_id(account_id) # pylint: disable=protected-access
return account

@classmethod
def get_eft_credit_balance(cls, account_id: int) -> Decimal:
"""Calculate pay account eft balance by account id."""
result = db.session.query(func.sum(EFTCreditModel.remaining_amount).label('credit_balance')) \
.filter(EFTCreditModel.payment_account_id == account_id) \
.group_by(EFTCreditModel.payment_account_id) \
.one_or_none()

return Decimal(result.credit_balance) if result else 0

@classmethod
def deduct_eft_credit(cls, account_id: int, invoice: InvoiceModel) -> Decimal:
"""Deduct EFT credit and update remaining credit records."""
eft_credits: List[EFTCreditModel] = db.session.query(EFTCreditModel) \
.filter(EFTCreditModel.remaining_amount > 0) \
.filter(EFTCreditModel.payment_account_id == account_id)\
.order_by(EFTCreditModel.created_on.asc())\
.all()

# Calculate invoice balance
invoice_balance = invoice.total - (invoice.paid or 0)

# Deduct credits and apply to the invoice
for eft_credit in eft_credits:
if eft_credit.remaining_amount >= invoice_balance:
# Credit covers the full invoice balance
eft_credit.remaining_amount -= invoice_balance
eft_credit.save()

invoice.paid = invoice.total
invoice.invoice_status_code = InvoiceStatus.PAID.value
invoice.save()

break

# Credit covers partial invoice balance
invoice_balance -= eft_credit.remaining_amount
invoice.paid += eft_credit.remaining_amount
invoice.invoice_status_code = InvoiceStatus.PARTIAL.value

eft_credit.remaining_amount = 0
eft_credit.save()

invoice.save()

@staticmethod
def _calculate_activation_date():
"""Find the activation date in local time.Convert it to UTC before returning."""
Expand Down
8 changes: 8 additions & 0 deletions pay-api/src/pay_api/services/payment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ def run_delete():
current_app.logger.debug('>accept_delete')


def _apply_eft_credits(payment_account: PaymentAccount, invoice: Invoice):
"""Apply EFT credits to invoice."""
eft_credit_balance = PaymentAccount.get_eft_credit_balance(payment_account.id)

if eft_credit_balance > 0:
PaymentAccount.deduct_eft_credit(payment_account.id, invoice)


def _calculate_fees(corp_type, filing_info):
"""Calculate and return the fees based on the filing type codes."""
fees = []
Expand Down
Loading

0 comments on commit 1a1cb0c

Please sign in to comment.