Skip to content

Commit

Permalink
21539 - Short name refund history support (#1757)
Browse files Browse the repository at this point in the history
  • Loading branch information
ochiu authored Sep 20, 2024
1 parent c4edd68 commit f89d475
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""EFT Short name history short name refund.
Revision ID: 67407611eec8
Revises: 423a9f909079
Create Date: 2024-09-18 10:20:15.689980
"""
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 = '67407611eec8'
down_revision = '423a9f909079'
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('eft_refund_id', sa.Integer(), nullable=True))
batch_op.create_index(batch_op.f('ix_eft_short_names_historical_eft_refund_id'), ['eft_refund_id'], unique=False)
batch_op.create_foreign_key('eft_short_names_historical_eft_refund_id_fkey', 'eft_refunds', ['eft_refund_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_eft_refund_id_fkey', type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_eft_short_names_historical_eft_refund_id'))
batch_op.drop_column('eft_refund_id')
30 changes: 30 additions & 0 deletions pay-api/migrations/versions/2024_09_19_29f59e6f147b_EFT_Refunds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""EFT Refunds additional columns
Revision ID: 29f59e6f147b
Revises: 67407611eec8
Create Date: 2024-09-19 16:09:53.120704
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.

revision = '29f59e6f147b'
down_revision = '67407611eec8'
branch_labels = None
depends_on = None


def upgrade():

with op.batch_alter_table('eft_refunds', schema=None) as batch_op:
batch_op.add_column(sa.Column('decline_reason', sa.String(), nullable=True))
batch_op.add_column(sa.Column('created_by', sa.String(length=100), nullable=True))


def downgrade():
with op.batch_alter_table('eft_refunds', schema=None) as batch_op:
batch_op.drop_column('created_by')
batch_op.drop_column('decline_reason')
4 changes: 4 additions & 0 deletions pay-api/src/pay_api/models/eft_refund.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class EFTRefund(BaseModel): # pylint: disable=too-many-instance-attributes
'auth_account_id',
'cas_supplier_number',
'comment',
'created_by',
'created_on',
'decline_reason',
'id',
'refund_amount',
'refund_email',
Expand All @@ -53,6 +55,8 @@ class EFTRefund(BaseModel): # pylint: disable=too-many-instance-attributes

cas_supplier_number = db.Column(db.String(), nullable=False)
comment = db.Column(db.String(), nullable=False)
decline_reason = db.Column(db.String(), nullable=True)
created_by = db.Column('created_by', db.String(100), nullable=True)
created_on = db.Column('created_on', db.DateTime, nullable=False, default=lambda: datetime.now(tz=timezone.utc))
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
refund_amount = db.Column(db.Numeric(), nullable=False)
Expand Down
7 changes: 7 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 @@ -45,6 +45,7 @@ class EFTShortnamesHistorical(BaseModel): # pylint:disable=too-many-instance-at
'created_on',
'credit_balance',
'description',
'eft_refund_id',
'hidden',
'invoice_id',
'is_processing',
Expand All @@ -61,6 +62,7 @@ class EFTShortnamesHistorical(BaseModel): # pylint:disable=too-many-instance-at
created_by = db.Column(db.String, nullable=True)
created_on = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(tz=timezone.utc))
credit_balance = db.Column(db.Numeric(19, 2), nullable=False)
eft_refund_id = db.Column(db.Integer, ForeignKey('eft_refunds.id'), nullable=True, index=True)
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)
Expand All @@ -76,6 +78,11 @@ def find_by_related_group_link_id(cls, group_link_id: int) -> Self:
"""Find historical records by related EFT Credit Invoice Link group id."""
return cls.query.filter_by(related_group_link_id=group_link_id).one_or_none()

@classmethod
def find_by_eft_refund_id(cls, eft_refund_id: int) -> Self:
"""Find historical records by EFT refund id."""
return cls.query.filter_by(eft_refund_id=eft_refund_id).one_or_none()


@define
class EFTShortnameHistorySchema: # pylint: disable=too-few-public-methods
Expand Down
14 changes: 11 additions & 3 deletions pay-api/src/pay_api/services/eft_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,23 @@ def create_shortname_refund(request: Dict[str, str], **kwargs) -> Dict[str, str]

current_app.logger.debug(f'Starting shortname refund : {shortname_id}')

refund = EftService._create_refund_model(request, shortname_id, amount, comment)
refund = EftService._create_refund_model(request, shortname_id, amount, comment).flush()
EftService._refund_eft_credits(int(shortname_id), amount)

EFTHistoryService.create_shortname_refund(
EFTHistory(short_name_id=shortname_id,
amount=amount,
credit_balance=EFTCreditModel.get_eft_credit_balance(int(shortname_id)),
eft_refund_id=refund.id,
is_processing=False,
hidden=False)).flush()

recipients = EFTRefundEmailList.find_all_emails()
subject = f'Pending Refund Request for Short Name {shortname}'
html_body = _render_shortname_details_body(shortname, amount, comment, shortname_id)

send_email(recipients, subject, html_body, **kwargs)
refund.save()
db.session.commit()

@staticmethod
def apply_payment_action(short_name_id: int, auth_account_id: str):
Expand Down Expand Up @@ -574,7 +582,7 @@ def _refund_eft_credits(shortname_id: int, amount: str):
credit.remaining_amount -= deduction
refund_amount -= deduction

credit.save()
credit.flush()

@staticmethod
def _create_refund_model(request: Dict[str, str],
Expand Down
17 changes: 17 additions & 0 deletions pay-api/src/pay_api/services/eft_short_name_historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class EFTShortnameHistory: # pylint: disable=too-many-instance-attributes
hidden: Optional[bool] = False
is_processing: Optional[bool] = False
invoice_id: Optional[int] = None
eft_refund_id: Optional[int] = None


@dataclass
Expand Down Expand Up @@ -124,6 +125,22 @@ def create_invoice_refund(history: EFTShortnameHistory, **kwargs) -> EFTShortnam
transaction_type=EFTHistoricalTypes.INVOICE_REFUND.value
)

@staticmethod
@user_context
def create_shortname_refund(history: EFTShortnameHistory, **kwargs) -> EFTShortnamesHistoricalModel:
"""Create EFT Short name 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,
short_name_id=history.short_name_id,
eft_refund_id=history.eft_refund_id,
transaction_date=EFTShortnameHistorical.transaction_date_now(),
transaction_type=EFTHistoricalTypes.SN_REFUND_PENDING_APPROVAL.value
)

@staticmethod
def transaction_date_now() -> datetime:
"""Construct transaction datetime using the utc timezone."""
Expand Down
5 changes: 5 additions & 0 deletions pay-api/src/pay_api/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ class EFTHistoricalTypes(Enum):
STATEMENT_PAID = 'STATEMENT_PAID'
STATEMENT_REVERSE = 'STATEMENT_REVERSE'

# Short name refund statuses
SN_REFUND_PENDING_APPROVAL = 'SN_REFUND_PENDING_APPROVAL'
SN_REFUND_APPROVED = 'SN_REFUND_APPROVED'
SN_REFUND_REJECTED = 'SN_REFUND_REJECTED'


class PaymentDetailsGlStatus(Enum):
"""Payment details GL status."""
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.22.2' # pylint: disable=invalid-name
__version__ = '1.22.3' # pylint: disable=invalid-name
60 changes: 39 additions & 21 deletions pay-api/tests/unit/api/test_eft_short_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,25 @@
import json
from datetime import datetime, timezone
from decimal import Decimal
from unittest.mock import patch

from pay_api.models import EFTCredit as EFTCreditModel
from pay_api.models import EFTCreditInvoiceLink as EFTCreditInvoiceModel
from pay_api.models import EFTFile as EFTFileModel
from pay_api.models import EFTShortnameLinks as EFTShortnameLinksModel
from pay_api.models import EFTShortnames as EFTShortnamesModel
from pay_api.models import EFTShortnamesHistorical as EFTShortnamesHistoryModel
from pay_api.models import EFTTransaction as EFTTransactionModel
from pay_api.models import PaymentAccount as PaymentAccountModel
from pay_api.models.eft_refund import EFTRefund as EFTRefundModel
from pay_api.services.eft_service import EftService
from pay_api.utils.enums import (
EFTCreditInvoiceStatus, EFTFileLineType, EFTProcessStatus, EFTShortnameRefundStatus, EFTShortnameStatus,
EFTShortnameType, InvoiceStatus, PaymentMethod, Role, StatementFrequency)
EFTCreditInvoiceStatus, EFTFileLineType, EFTHistoricalTypes, EFTProcessStatus, EFTShortnameRefundStatus,
EFTShortnameStatus, EFTShortnameType, InvoiceStatus, PaymentMethod, Role, StatementFrequency)
from pay_api.utils.errors import Error
from tests.utilities.base_test import (
factory_eft_file, factory_eft_shortname, factory_eft_shortname_link, factory_invoice, factory_payment_account,
factory_statement, factory_statement_invoices, factory_statement_settings, get_claims, token_header)
factory_eft_credit, factory_eft_file, factory_eft_shortname, factory_eft_shortname_link, factory_invoice,
factory_payment_account, factory_statement, factory_statement_invoices, factory_statement_settings, get_claims,
token_header)


def test_create_eft_short_name_link(session, client, jwt, app):
Expand Down Expand Up @@ -897,28 +899,44 @@ def test_search_eft_short_names(session, client, jwt, app):
data_dict['single-linked']['statement_summary'][0])


def test_post_shortname_refund_success(client, mocker, jwt, app):
def test_post_shortname_refund_success(db, session, client, jwt, app):
"""Test successful creation of a shortname refund."""
token = jwt.create_jwt(get_claims(roles=[Role.EFT_REFUND.value]), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

mock_create_shortname_refund = mocker.patch.object(EftService, 'create_shortname_refund')
mock_create_shortname_refund.return_value = {'refundId': '12345'}
payment_account = factory_payment_account(payment_method_code=PaymentMethod.EFT.value,
auth_account_id='1234').save()
eft_file = factory_eft_file().save()
short_name = factory_eft_shortname(short_name='TESTSHORTNAME').save()
factory_eft_credit(eft_file_id=eft_file.id, short_name_id=short_name.id, amount=100, remaining_amount=100).save()

data = {
'shortNameId': '12345',
'authAccountId': '123',
'refundAmount': 100.00,
'casSupplierNum': 'CAS123',
'refundEmail': 'test@example.com',
'comment': 'Refund for overpayment'
}

rv = client.post('/api/v1/eft-shortnames/shortname-refund', headers=headers, json=data)

assert rv.status_code == 202
assert rv.json == {'refundId': '12345'}
mock_create_shortname_refund.assert_called_once_with(data)
'shortNameId': short_name.id,
'authAccountId': payment_account.auth_account_id,
'refundAmount': 100.00,
'casSupplierNum': 'CAS123',
'refundEmail': 'test@example.com',
'comment': 'Refund for overpayment'
}
with patch('pay_api.services.eft_service.send_email') as mock_email:
rv = client.post('/api/v1/eft-shortnames/shortname-refund', headers=headers, json=data)
assert rv.status_code == 202
mock_email.assert_called_once()

eft_refund = db.session.query(EFTRefundModel).one_or_none()
assert eft_refund.id is not None
assert eft_refund.short_name_id == short_name.id
assert eft_refund.refund_amount == 100.00
assert eft_refund.cas_supplier_number == 'CAS123'
assert eft_refund.refund_email == 'test@example.com'
assert eft_refund.comment == 'Refund for overpayment'

history_record = db.session.query(EFTShortnamesHistoryModel).one_or_none()
assert history_record is not None
assert history_record.amount == 100
assert history_record.eft_refund_id == eft_refund.id
assert history_record.credit_balance == 0
assert history_record.transaction_type == EFTHistoricalTypes.SN_REFUND_PENDING_APPROVAL.value


def test_post_shortname_refund_invalid_request(client, mocker, jwt, app):
Expand Down
16 changes: 9 additions & 7 deletions pay-api/tests/unit/models/test_eft_refund.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,26 @@ def test_eft_refund_defaults(session):
def test_eft_refund_all_attributes(session):
"""Assert all EFT refund attributes are stored."""
# Ensure the required entry exists in the related table
short_name = factory_eft_shortname(short_name='Test Short Name')
db.session.add(short_name)
db.session.commit()
short_name_id = short_name.id

short_name = factory_eft_shortname(short_name='Test Short Name').save()
refund_amount = 150.00
cas_supplier_number = 'SUP654321'
refund_email = 'updated@example.com'
comment = 'Updated comment'
status = 'COMPLETED'
created_by = 'user111'
decline_reason = 'Decline reason comment'
updated_by = 'user123'
updated_by_name = 'User Name'

eft_refund = EFTRefundModel(
short_name_id=short_name_id,
short_name_id=short_name.id,
refund_amount=refund_amount,
cas_supplier_number=cas_supplier_number,
refund_email=refund_email,
comment=comment,
decline_reason=decline_reason,
status=status,
created_by=created_by,
updated_by=updated_by,
updated_by_name=updated_by_name,
)
Expand All @@ -86,11 +86,13 @@ def test_eft_refund_all_attributes(session):
eft_refund = db.session.query(EFTRefundModel).filter(EFTRefundModel.id == eft_refund.id).one_or_none()

assert eft_refund is not None
assert eft_refund.short_name_id == short_name_id
assert eft_refund.short_name_id == short_name.id
assert eft_refund.refund_amount == refund_amount
assert eft_refund.cas_supplier_number == cas_supplier_number
assert eft_refund.refund_email == refund_email
assert eft_refund.comment == comment
assert eft_refund.decline_reason == decline_reason
assert eft_refund.status == status
assert eft_refund.created_by == created_by
assert eft_refund.updated_by == updated_by
assert eft_refund.updated_by_name == updated_by_name
Loading

0 comments on commit f89d475

Please sign in to comment.