Skip to content

Commit

Permalink
21597-EFT History Invoice Refund (bcgov#1685)
Browse files Browse the repository at this point in the history
  • Loading branch information
ochiu authored Aug 15, 2024
1 parent 4d48bfa commit 122634f
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Invoice refund support for eft_short_names_historical
Revision ID: 4410b7fc6437
Revises: 5cb9c5f5896c
Create Date: 2024-08-14 10:42:13.484178
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
# Note you may see foreign keys with distribution_codes_history
# For disbursement_distribution_code_id, service_fee_distribution_code_id
# Please ignore those lines and don't include in migration.

revision = '4410b7fc6437'
down_revision = '5cb9c5f5896c'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table('eft_short_names_historical', schema=None) as batch_op:
batch_op.add_column(sa.Column('invoice_id', sa.Integer(), nullable=True))
batch_op.create_index(batch_op.f('ix_eft_short_names_historical_invoice_id'), ['invoice_id'], unique=False)
batch_op.create_foreign_key('eft_short_names_historical_invoice_id_fkey', 'invoices', ['invoice_id'], ['id'])


def downgrade():
with op.batch_alter_table('eft_short_names_historical', schema=None) as batch_op:
batch_op.drop_constraint('eft_short_names_historical_invoice_id_fkey', type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_eft_short_names_historical_invoice_id'))
batch_op.drop_column('invoice_id')
4 changes: 4 additions & 0 deletions pay-api/src/pay_api/models/eft_short_names_historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class EFTShortnamesHistorical(BaseModel): # pylint:disable=too-many-instance-at
'credit_balance',
'description',
'hidden',
'invoice_id',
'is_processing',
'payment_account_id',
'related_group_link_id',
Expand All @@ -61,6 +62,7 @@ class EFTShortnamesHistorical(BaseModel): # pylint:disable=too-many-instance-at
created_on = db.Column(db.DateTime, nullable=False, default=datetime.now(tz=timezone.utc))
credit_balance = db.Column(db.Numeric(19, 2), nullable=False)
hidden = db.Column(db.Boolean(), nullable=False, default=False, index=True)
invoice_id = db.Column(db.Integer, ForeignKey('invoices.id'), nullable=True, index=True)
is_processing = db.Column(db.Boolean(), nullable=False, default=False)
payment_account_id = db.Column(db.Integer, ForeignKey('payment_accounts.id'), nullable=True, index=True)
related_group_link_id = db.Column(db.Integer, nullable=True, index=True)
Expand All @@ -84,6 +86,7 @@ class EFTShortnameHistorySchema: # pylint: disable=too-few-public-methods
account_name: str
account_branch: str
amount: Decimal
invoice_id: int
statement_number: int
short_name_id: int
short_name_balance: Decimal
Expand All @@ -105,6 +108,7 @@ def from_row(cls, row):
account_id=getattr(row, 'auth_account_id', None),
account_name=getattr(row, 'account_name', None),
account_branch=getattr(row, 'account_branch', None),
invoice_id=getattr(row, 'invoice_id', None),
statement_number=getattr(row, 'statement_number', None),
transaction_date=getattr(row, 'transaction_date', None),
transaction_type=getattr(row, 'transaction_type', None),
Expand Down
24 changes: 23 additions & 1 deletion pay-api/src/pay_api/services/eft_short_name_historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class EFTShortnameHistory: # pylint: disable=too-many-instance-attributes
statement_number: Optional[int] = None
hidden: Optional[bool] = False
is_processing: Optional[bool] = False
invoice_id: Optional[int] = None


@dataclass
Expand Down Expand Up @@ -104,6 +105,25 @@ def create_statement_reverse(history: EFTShortnameHistory, **kwargs) -> EFTShort
transaction_type=EFTHistoricalTypes.STATEMENT_REVERSE.value
)

@staticmethod
@user_context
def create_invoice_refund(history: EFTShortnameHistory, **kwargs) -> EFTShortnamesHistoricalModel:
"""Create EFT Short name invoice refund historical record."""
return EFTShortnamesHistoricalModel(
amount=history.amount,
created_by=kwargs['user'].user_name,
credit_balance=history.credit_balance,
hidden=history.hidden,
is_processing=history.is_processing,
payment_account_id=history.payment_account_id,
related_group_link_id=history.related_group_link_id,
short_name_id=history.short_name_id,
statement_number=history.statement_number,
invoice_id=history.invoice_id,
transaction_date=EFTShortnameHistorical.transaction_date_now(),
transaction_type=EFTHistoricalTypes.INVOICE_REFUND.value
)

@staticmethod
def transaction_date_now() -> datetime:
"""Construct transaction datetime using the utc timezone."""
Expand Down Expand Up @@ -132,7 +152,8 @@ def search(cls, short_name_id: int,
)
.where(
latest_history_model.short_name_id == history_model.short_name_id,
latest_history_model.statement_number == history_model.statement_number
latest_history_model.statement_number == history_model.statement_number,
latest_history_model.transaction_type != EFTHistoricalTypes.INVOICE_REFUND.value
)
.order_by(
latest_history_model.statement_number,
Expand Down Expand Up @@ -169,6 +190,7 @@ def search(cls, short_name_id: int,
history_model.short_name_id,
history_model.amount,
history_model.credit_balance,
history_model.invoice_id,
history_model.statement_number,
history_model.transaction_date,
history_model.transaction_type,
Expand Down
29 changes: 20 additions & 9 deletions pay-api/src/pay_api/services/eft_short_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,19 @@ def _apply_payment_action(cls, short_name_id: int, auth_account_id: str):
@classmethod
def _get_statement_credit_invoice_links(cls, shortname_id, statement_id) -> List[EFTCreditInvoiceLinkModel]:
"""Get most recent EFT Credit invoice links associated to a statement and short name."""
return (db.session.query(EFTCreditInvoiceLinkModel)
.distinct(EFTCreditInvoiceLinkModel.invoice_id)
.join(EFTCreditModel, EFTCreditModel.id == EFTCreditInvoiceLinkModel.eft_credit_id)
.join(StatementInvoicesModel, StatementInvoicesModel.invoice_id == EFTCreditInvoiceLinkModel.invoice_id)
.filter(StatementInvoicesModel.statement_id == statement_id)
.filter(EFTCreditModel.short_name_id == shortname_id)
.filter(EFTCreditInvoiceLinkModel.status_code != EFTCreditInvoiceStatus.CANCELLED.value)
.order_by(EFTCreditInvoiceLinkModel.invoice_id, EFTCreditInvoiceLinkModel.created_on.desc())
).all()
query = (db.session.query(EFTCreditInvoiceLinkModel)
.distinct(EFTCreditInvoiceLinkModel.invoice_id)
.join(EFTCreditModel, EFTCreditModel.id == EFTCreditInvoiceLinkModel.eft_credit_id)
.join(StatementInvoicesModel,
StatementInvoicesModel.invoice_id == EFTCreditInvoiceLinkModel.invoice_id)
.filter(StatementInvoicesModel.statement_id == statement_id)
.filter(EFTCreditModel.short_name_id == shortname_id)
.filter(EFTCreditInvoiceLinkModel.status_code != EFTCreditInvoiceStatus.CANCELLED.value)
.order_by(EFTCreditInvoiceLinkModel.invoice_id.desc(),
EFTCreditInvoiceLinkModel.created_on.desc(),
EFTCreditInvoiceLinkModel.id.desc())
)
return query.all()

@classmethod
def _validate_reversal_credit_invoice_links(cls, statement_id: int,
Expand Down Expand Up @@ -280,11 +284,18 @@ def _reverse_payment_action(cls, short_name_id: int, statement_id: int):
credit_invoice_links = cls._get_statement_credit_invoice_links(short_name_id, statement_id)
cls._validate_reversal_credit_invoice_links(statement_id, credit_invoice_links)

alt_flow_invoice_statuses = [InvoiceStatus.REFUND_REQUESTED.value,
InvoiceStatus.REFUNDED.value,
InvoiceStatus.CANCELLED.value]
link_group_id = EFTCreditInvoiceLinkModel.get_next_group_link_seq()
reversed_credits = 0
for current_link in credit_invoice_links:
invoice = InvoiceModel.find_by_id(current_link.invoice_id)

# Check if the invoice status is handled by other flows and can be skipped
if invoice.invoice_status_code in alt_flow_invoice_statuses:
continue

if invoice.invoice_status_code != InvoiceStatus.PAID.value:
current_app.logger.error(f'EFT Invoice Payment could not be reversed for invoice '
f'- {invoice.id} in status {invoice.invoice_status_code}.')
Expand Down
1 change: 1 addition & 0 deletions pay-api/src/pay_api/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ class EFTHistoricalTypes(Enum):
"""EFT Short names historical transaction types."""

FUNDS_RECEIVED = 'FUNDS_RECEIVED'
INVOICE_REFUND = 'INVOICE_REFUND'
STATEMENT_PAID = 'STATEMENT_PAID'
STATEMENT_REVERSE = 'STATEMENT_REVERSE'

Expand Down
2 changes: 1 addition & 1 deletion pay-api/src/pay_api/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
Development release segment: .devN
"""

__version__ = '1.21.8' # pylint: disable=invalid-name
__version__ = '1.21.9' # pylint: disable=invalid-name
79 changes: 63 additions & 16 deletions pay-api/tests/unit/api/test_eft_short_name_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@

from pay_api.services.eft_short_name_historical import EFTShortnameHistorical as EFTHistoryService
from pay_api.services.eft_short_name_historical import EFTShortnameHistory as EFTHistory
from pay_api.utils.enums import EFTHistoricalTypes, PaymentMethod, Role
from tests.utilities.base_test import factory_eft_shortname, factory_payment_account, get_claims, token_header
from pay_api.utils.enums import EFTHistoricalTypes, InvoiceStatus, PaymentMethod, Role
from tests.utilities.base_test import (
factory_eft_shortname, factory_invoice, factory_payment_account, get_claims, token_header)


def setup_test_data(exclude_history: bool = False):
Expand Down Expand Up @@ -57,10 +58,10 @@ def setup_test_data(exclude_history: bool = False):

@pytest.mark.parametrize('result_index, expected_values', [
(0, {'isReversible': False, 'accountBranch': 'BRANCH', 'accountName': 'ABC', 'amount': 351.50,
'shortNameBalance': 351.50, 'statementNumber': 1234,
'shortNameBalance': 351.50, 'statementNumber': 1234, 'invoiceId': None,
'transactionType': EFTHistoricalTypes.STATEMENT_REVERSE.value}),
(1, {'isReversible': False, 'accountBranch': 'BRANCH', 'accountName': 'ABC', 'amount': 351.50,
'shortNameBalance': 0, 'statementNumber': 1234,
'shortNameBalance': 0, 'statementNumber': 1234, 'invoiceId': None,
'transactionType': EFTHistoricalTypes.STATEMENT_PAID.value})
])
def test_search_statement_history(session, result_index, expected_values, client, jwt, app):
Expand All @@ -82,18 +83,19 @@ def test_search_statement_history(session, result_index, expected_values, client
assert len(result_dict['items']) == 3

transaction_date = EFTHistoryService.transaction_date_now().strftime('%Y-%m-%dT%H:%M:%S')
statement_paid = result_dict['items'][result_index]
assert statement_paid['historicalId'] is not None
assert statement_paid['isReversible'] == expected_values['isReversible']
assert statement_paid['accountId'] == payment_account.auth_account_id
assert statement_paid['accountBranch'] == expected_values['accountBranch']
assert statement_paid['accountName'] == expected_values['accountName']
assert statement_paid['amount'] == expected_values['amount']
assert statement_paid['shortNameBalance'] == expected_values['shortNameBalance']
assert statement_paid['shortNameId'] == short_name.id
assert statement_paid['statementNumber'] == expected_values['statementNumber']
assert statement_paid['transactionType'] == expected_values['transactionType']
assert statement_paid['transactionDate'] == transaction_date
statement_history = result_dict['items'][result_index]
assert statement_history['historicalId'] is not None
assert statement_history['isReversible'] == expected_values['isReversible']
assert statement_history['accountId'] == payment_account.auth_account_id
assert statement_history['accountBranch'] == expected_values['accountBranch']
assert statement_history['accountName'] == expected_values['accountName']
assert statement_history['amount'] == expected_values['amount']
assert statement_history['shortNameBalance'] == expected_values['shortNameBalance']
assert statement_history['shortNameId'] == short_name.id
assert statement_history['invoiceId'] == expected_values['invoiceId']
assert statement_history['statementNumber'] == expected_values['statementNumber']
assert statement_history['transactionType'] == expected_values['transactionType']
assert statement_history['transactionDate'] == transaction_date


def test_search_funds_received_history(session, client, jwt, app):
Expand Down Expand Up @@ -125,10 +127,55 @@ def test_search_funds_received_history(session, client, jwt, app):
assert funds_received['shortNameBalance'] == 351.50
assert funds_received['shortNameId'] == short_name.id
assert funds_received['statementNumber'] is None
assert funds_received['invoiceId'] is None
assert funds_received['transactionType'] == EFTHistoricalTypes.FUNDS_RECEIVED.value
assert funds_received['transactionDate'] == transaction_date


def test_search_invoice_refund_history(session, client, jwt, app):
"""Assert that EFT short names invoice refund history can be searched."""
token = jwt.create_jwt(get_claims(roles=[Role.MANAGE_EFT.value]), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}
transaction_date = datetime(2024, 7, 31, 0, 0, 0)
with freeze_time(transaction_date):
payment_account, short_name = setup_test_data(exclude_history=True)
invoice = factory_invoice(payment_account, payment_method_code=PaymentMethod.EFT.value,
status_code=InvoiceStatus.REFUND_REQUESTED.value,
total=50, paid=50).save()

EFTHistoryService.create_invoice_refund(EFTHistory(short_name_id=short_name.id,
amount=50,
credit_balance=50,
payment_account_id=payment_account.id,
related_group_link_id=1,
statement_number=1234,
invoice_id=invoice.id)).save()

rv = client.get(f'/api/v1/eft-shortnames/{short_name.id}/history', headers=headers)
result_dict = rv.json
assert result_dict is not None
assert result_dict['page'] == 1
assert result_dict['total'] == 1
assert result_dict['limit'] == 10
assert result_dict['items'] is not None
assert len(result_dict['items']) == 1

transaction_date = EFTHistoryService.transaction_date_now().strftime('%Y-%m-%dT%H:%M:%S')
invoice_refund = result_dict['items'][0]
assert invoice_refund['historicalId'] is not None
assert invoice_refund['isReversible'] is False
assert invoice_refund['accountId'] == payment_account.auth_account_id
assert invoice_refund['accountBranch'] == payment_account.branch_name
assert invoice_refund['accountName'] == 'ABC'
assert invoice_refund['amount'] == 50
assert invoice_refund['shortNameBalance'] == 50
assert invoice_refund['shortNameId'] == short_name.id
assert invoice_refund['invoiceId'] == invoice.id
assert invoice_refund['statementNumber'] == 1234
assert invoice_refund['transactionType'] == EFTHistoricalTypes.INVOICE_REFUND.value
assert invoice_refund['transactionDate'] == transaction_date


def test_search_statement_paid_is_reversible(session, client, jwt, app):
"""Assert that EFT short names statement paid is reversible."""
token = jwt.create_jwt(get_claims(roles=[Role.MANAGE_EFT.value]), token_header)
Expand Down

0 comments on commit 122634f

Please sign in to comment.