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

20787 - allow specific action overrides #1763

Merged
merged 7 commits into from
Oct 3, 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
23 changes: 21 additions & 2 deletions DEVELOPER_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,24 @@ WHERE pid in (146105,
146355,
146394
);



23. How do I use overrides to send EFT statement reminders/due notifications or set overdue on invoices?

Provide the override action, date override and auth account id. If there is no accountId, it will be applied environment wide.
`python3 invoke_jobs.py STATEMENTS_DUE <action> <dateOverride> <accountId>`

Supported Actions:

NOTIFICATION - send payment reminder or due notification based on date override.
e.g. Payment Reminder (date override should be 7 days before the last day)
`python3 invoke_jobs.py STATEMENTS_DUE NOTIFICATION 2024-10-24 1234`

e.g. Payment Due (date override should be on the last day of the month)
`python3 invoke_jobs.py STATEMENTS_DUE NOTIFICATION 2024-10-31 1234`

OVERDUE - set invoices that are overdue to overdue status
e.g. Set overdue status for overdue invoices on auth account 1234.
`python3 invoke_jobs.py STATEMENTS_DUE OVERDUE 2024-10-15 1234`

Date Override: The date you want to emulate the job is running on.
Account Id: The auth account id to run the job against.
10 changes: 7 additions & 3 deletions jobs/payment-jobs/invoke_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,13 @@ def run(job_name, argument=None):
UnpaidInvoiceNotifyTask.notify_unpaid_invoices()
application.logger.info('<<<< Completed Sending notification for OB invoices >>>>')
case 'STATEMENTS_DUE':
action_date_override = argument[0] if len(argument) >= 1 else None
auth_account_override = argument[1] if len(argument) >= 2 else None
StatementDueTask.process_unpaid_statements(action_date_override=action_date_override,
action_override = argument[0] if len(argument) >= 1 else None
date_override = argument[1] if len(argument) >= 2 else None
auth_account_override = argument[2] if len(argument) >= 3 else None

application.logger.info(f'{action_override} {date_override} {auth_account_override}')
StatementDueTask.process_unpaid_statements(action_override=action_override,
date_override=date_override,
auth_account_override=auth_account_override)
application.logger.info(
'<<<< Completed Sending notification for unpaid statements >>>>')
Expand Down
45 changes: 37 additions & 8 deletions jobs/payment-jobs/tasks/statement_due_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Task to notify user for any outstanding statement."""
from calendar import monthrange
from datetime import datetime, timedelta, timezone
from dateutil.relativedelta import relativedelta

import pytz
from flask import current_app
Expand Down Expand Up @@ -57,23 +57,46 @@ class StatementDueTask: # pylint: disable=too-few-public-methods
statement_date_override = None

@classmethod
def process_unpaid_statements(cls, action_date_override=None,
def process_override_command(cls, action, date_override):
"""Process override action."""
if date_override is None:
current_app.logger.error(f'Expecting date override for action: {action}.')

date_override = datetime.strptime(date_override, '%Y-%m-%d')
match action:
case 'NOTIFICATION':
cls.action_date_override = date_override.date()
cls.statement_date_override = date_override
cls._notify_for_monthly()
case 'OVERDUE':
cls.action_date_override = date_override
cls._update_invoice_overdue_status()
case _:
current_app.logger.error(f'Unsupported action override: {action}.')

@classmethod
def process_unpaid_statements(cls,
action_override=None,
date_override=None,
auth_account_override=None, statement_date_override=None):
"""Notify for unpaid statements with an amount owing."""
eft_enabled = flags.is_on('enable-eft-payment-method', default=False)
if eft_enabled:
cls.action_date_override = action_date_override
cls.auth_account_override = auth_account_override
cls.statement_date_override = statement_date_override
cls._update_invoice_overdue_status()
cls._notify_for_monthly()

if action_override is not None and len(action_override.strip()) > 0:
cls.process_override_command(action_override, date_override)
else:
cls._update_invoice_overdue_status()
cls._notify_for_monthly()

@classmethod
def _update_invoice_overdue_status(cls):
"""Update the status of any invoices that are overdue."""
# Needs to be non timezone aware.
if cls.action_date_override:
now = datetime.strptime(cls.action_date_override, '%Y-%m-%d').replace(hour=8)
now = cls.action_date_override.replace(hour=8)
offset_hours = -now.astimezone(pytz.timezone('America/Vancouver')).utcoffset().total_seconds() / 60 / 60
now = now.replace(hour=int(offset_hours), minute=0, second=0)
else:
Expand Down Expand Up @@ -197,12 +220,12 @@ def _determine_action_and_due_date_by_invoice(cls, statement: StatementModel):
# 3. 7 day reminder Feb 21th (due date - 7)
# 4. Final reminder Feb 28th (due date client should be told to pay by this time)
# 5. Overdue Date and account locked March 15th
day_invoice_due = statement.to_date + relativedelta(months=1)
day_invoice_due = cls._get_last_day_of_next_month(statement.to_date)
seven_days_before_invoice_due = day_invoice_due - timedelta(days=7)

# Needs to be non timezone aware for comparison.
if cls.action_date_override:
now_date = datetime.strptime(cls.action_date_override, '%Y-%m-%d').date()
now_date = cls.action_date_override
else:
now_date = datetime.now(tz=timezone.utc).replace(tzinfo=None).date()

Expand All @@ -222,3 +245,9 @@ def _determine_recipient_emails(cls, statement: StatementRecipientsModel) -> str

current_app.logger.error(f'No recipients found for payment_account_id: {statement.payment_account_id}. Skip.')
return None

@classmethod
def _get_last_day_of_next_month(cls, date):
"""Find the last day of the next month."""
next_month = date.replace(day=1) + timedelta(days=32)
return next_month.replace(day=monthrange(next_month.year, next_month.month)[1])
56 changes: 56 additions & 0 deletions jobs/payment-jobs/tests/jobs/test_statement_due_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,59 @@ def test_overdue_invoices_updated(setup, session):
StatementDueTask.process_unpaid_statements(auth_account_override=account.auth_account_id)
assert invoice.invoice_status_code == InvoiceStatus.OVERDUE.value
assert invoice2.invoice_status_code == InvoiceStatus.APPROVED.value


@pytest.mark.parametrize('test_name, date_override, action', [
('reminder', '2023-02-21', StatementNotificationAction.REMINDER),
('due', '2023-02-28', StatementNotificationAction.DUE),
('overdue', '2023-03-15', StatementNotificationAction.OVERDUE)
])
def test_statement_due_overrides(setup, session, test_name, date_override, action):
"""Assert payment reminder event is being sent."""
account, invoice, _, \
statement_recipient, _ = create_test_data(PaymentMethod.EFT.value,
datetime(2023, 1, 1, 8), # Hour 0 doesnt work for CI
StatementFrequency.MONTHLY.value,
351.50)
assert invoice.payment_method_code == PaymentMethod.EFT.value
assert invoice.overdue_date
assert account.payment_method == PaymentMethod.EFT.value

# Generate statements runs at 8:01 UTC, currently set to 7:01 UTC, should be moved.
with freeze_time(datetime(2023, 2, 1, 8, 0, 1)):
StatementTask.generate_statements()

statements = StatementService.get_account_statements(auth_account_id=account.auth_account_id, page=1, limit=100)
assert statements is not None
assert len(statements) == 2 # items results and page total
assert len(statements[0]) == 1 # items
invoices = StatementInvoicesModel.find_all_invoices_for_statement(statements[0][0].id)
assert invoices is not None
assert invoices[0].invoice_id == invoice.id

summary = StatementService.get_summary(account.auth_account_id, statements[0][0].id)
total_amount_owing = summary['total_due']

with patch('utils.auth_event.AuthEvent.publish_lock_account_event') as mock_auth_event:
with patch('tasks.statement_due_task.publish_payment_notification') as mock_mailer:
# Statement due task looks at the month before.
if test_name == 'overdue':
StatementDueTask.process_unpaid_statements(action_override='OVERDUE',
date_override=date_override)

StatementDueTask.process_unpaid_statements(action_override='NOTIFICATION',
date_override=date_override)
if action == StatementNotificationAction.OVERDUE:
mock_auth_event.assert_called()
assert statements[0][0].overdue_notification_date
assert NonSufficientFundsModel.find_by_invoice_id(invoice.id)
assert account.has_overdue_invoices
else:
due_date = statements[0][0].to_date + relativedelta(months=1)
mock_mailer.assert_called_with(StatementNotificationInfo(auth_account_id=account.auth_account_id,
statement=statements[0][0],
action=action,
due_date=due_date,
emails=statement_recipient.email,
total_amount_owing=total_amount_owing,
short_name_links_count=0))
2 changes: 1 addition & 1 deletion jobs/payment-jobs/utils/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def publish_statement_notification(pay_account: PaymentAccountModel, statement:
def publish_payment_notification(info: StatementNotificationInfo) -> bool:
"""Publish payment notification message to the mailer queue."""
message_type = QueueMessageTypes.PAYMENT_DUE_NOTIFICATION.value \
if info.action == StatementNotificationAction.OVERDUE \
if info.action in [StatementNotificationAction.DUE, StatementNotificationAction.OVERDUE] \
else QueueMessageTypes.PAYMENT_REMINDER_NOTIFICATION.value

payload = {
Expand Down
Loading