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

21539 - Short name refund history support #1757

Merged
merged 2 commits into from
Sep 20, 2024
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
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')
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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When implementing approve/decline we need to find this history record by eft_refund_id and update the status.

For decline, we will need to return the refund amount back to the short name as well

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
Loading