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 1 commit
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')
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
79 changes: 77 additions & 2 deletions pay-api/tests/unit/services/test_eft_short_name_historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@

from pay_api.services.eft_short_name_historical import EFTShortnameHistorical as EFTShortnameHistoryService
from pay_api.services.eft_short_name_historical import EFTShortnameHistory
from pay_api.utils.enums import EFTHistoricalTypes
from tests.utilities.base_test import factory_eft_shortname, factory_payment_account
from pay_api.utils.enums import EFTHistoricalTypes, InvoiceStatus, PaymentMethod
from tests.utilities.base_test import (
factory_eft_refund, factory_eft_shortname, factory_invoice, factory_payment_account)


def setup_test_data():
Expand Down Expand Up @@ -60,6 +61,8 @@ def test_create_funds_received(session):
assert historical_record.payment_account_id is None
assert historical_record.related_group_link_id is None
assert historical_record.short_name_id == short_name.id
assert historical_record.eft_refund_id is None
assert historical_record.invoice_id is None
assert historical_record.statement_number is None
assert historical_record.transaction_date.replace(microsecond=0) == transaction_date
assert historical_record.transaction_type == EFTHistoricalTypes.FUNDS_RECEIVED.value
Expand Down Expand Up @@ -90,6 +93,8 @@ def test_create_statement_paid(session, staff_user_mock):
assert historical_record.payment_account_id == payment_account.id
assert historical_record.related_group_link_id == 1
assert historical_record.short_name_id == short_name.id
assert historical_record.eft_refund_id is None
assert historical_record.invoice_id is None
assert historical_record.statement_number == 1234567
assert historical_record.transaction_date == transaction_date
assert historical_record.transaction_type == EFTHistoricalTypes.STATEMENT_PAID.value
Expand Down Expand Up @@ -121,6 +126,76 @@ def test_create_statement_reverse(session, staff_user_mock):
assert historical_record.payment_account_id == payment_account.id
assert historical_record.related_group_link_id == 1
assert historical_record.short_name_id == short_name.id
assert historical_record.eft_refund_id is None
assert historical_record.invoice_id is None
assert historical_record.statement_number == 1234567
assert historical_record.transaction_date == transaction_date
assert historical_record.transaction_type == EFTHistoricalTypes.STATEMENT_REVERSE.value


def test_create_invoice_refund(session, staff_user_mock):
ochiu marked this conversation as resolved.
Show resolved Hide resolved
"""Test create short name invoice refund history."""
transaction_date = datetime(2024, 7, 31, 0, 0, 0)
with freeze_time(transaction_date):
payment_account, short_name = setup_test_data()
invoice = factory_invoice(payment_account, payment_method_code=PaymentMethod.EFT.value,
status_code=InvoiceStatus.APPROVED.value,
total=50).save()
history = EFTShortnameHistory(
amount=151.50,
credit_balance=300,
payment_account_id=payment_account.id,
short_name_id=short_name.id,
invoice_id=invoice.id,
statement_number=1234567,
related_group_link_id=1
)
historical_record = EFTShortnameHistoryService.create_invoice_refund(history)
historical_record.save()
assert historical_record.id is not None
assert historical_record.amount == 151.50
assert historical_record.created_on is not None
assert historical_record.created_by == 'STAFF USER'
assert historical_record.credit_balance == 300
assert not historical_record.hidden
assert not historical_record.is_processing
assert historical_record.payment_account_id == payment_account.id
assert historical_record.related_group_link_id == 1
assert historical_record.short_name_id == short_name.id
assert historical_record.eft_refund_id is None
assert historical_record.invoice_id == invoice.id
assert historical_record.statement_number == 1234567
assert historical_record.transaction_date == transaction_date
assert historical_record.transaction_type == EFTHistoricalTypes.INVOICE_REFUND.value


def test_create_short_name_refund(session, staff_user_mock):
"""Test create short name refund history."""
transaction_date = datetime(2024, 7, 31, 0, 0, 0)
with freeze_time(transaction_date):
payment_account, short_name = setup_test_data()
eft_refund = factory_eft_refund(short_name.id, refund_amount=100).save()
history = EFTShortnameHistory(
amount=151.50,
credit_balance=300,
short_name_id=short_name.id,
eft_refund_id=eft_refund.id,
statement_number=1234567
)
historical_record = EFTShortnameHistoryService.create_shortname_refund(history)
historical_record.save()
assert historical_record.id is not None
assert historical_record.amount == 151.50
assert historical_record.created_on is not None
assert historical_record.created_by == 'STAFF USER'
assert historical_record.credit_balance == 300
assert not historical_record.hidden
assert not historical_record.is_processing
assert historical_record.payment_account_id is None
assert historical_record.related_group_link_id is None
assert historical_record.short_name_id == short_name.id
assert historical_record.statement_number is None
assert historical_record.eft_refund_id == eft_refund.id
assert historical_record.invoice_id is None
assert historical_record.transaction_date == transaction_date
assert historical_record.transaction_type == EFTHistoricalTypes.SN_REFUND_PENDING_APPROVAL.value
14 changes: 13 additions & 1 deletion pay-api/tests/utilities/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from faker import Faker

from pay_api.models import (
CfsAccount, Comment, DistributionCode, DistributionCodeLink, EFTCredit, EFTCreditInvoiceLink, EFTFile,
CfsAccount, Comment, DistributionCode, DistributionCodeLink, EFTCredit, EFTCreditInvoiceLink, EFTFile, EFTRefund,
EFTShortnameLinks, EFTShortnames, Invoice, InvoiceReference, NonSufficientFunds, Payment, PaymentAccount,
PaymentLineItem, PaymentTransaction, Receipt, RoutingSlip, Statement, StatementInvoices, StatementSettings)
from pay_api.utils.constants import DT_SHORT_FORMAT
Expand Down Expand Up @@ -931,6 +931,18 @@ def factory_eft_credit(eft_file_id, short_name_id, amount=10.00, remaining_amoun
)


def factory_eft_refund(short_name_id, refund_amount, cas_supplier_number='1234567',
refund_email='test@test.com', comment='test comment'):
"""Return an EFT Refund."""
return EFTRefund(
short_name_id=short_name_id,
refund_amount=refund_amount,
cas_supplier_number=cas_supplier_number,
refund_email=refund_email,
comment=comment
)


def factory_eft_credit_invoice_link(eft_credit_id, invoice_id, status_code, amount=1, link_group_id=None):
"""Return an EFT Credit invoice link."""
return EFTCreditInvoiceLink(eft_credit_id=eft_credit_id,
Expand Down
Loading