-
Notifications
You must be signed in to change notification settings - Fork 39
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
18574 - EFT Verification Fixes #1328
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,11 +24,11 @@ | |
from pay_api.services.flags import flags | ||
from pay_api.services.statement import Statement | ||
from pay_api.utils.enums import InvoiceStatus, PaymentMethod, StatementFrequency | ||
from pay_api.utils.util import current_local_time, get_first_and_last_dates_of_month | ||
from pay_api.utils.util import current_local_time, get_local_time | ||
from sentry_sdk import capture_message | ||
from sqlalchemy import Date | ||
from sqlalchemy import func | ||
|
||
from utils.mailer import publish_payment_notification | ||
from utils.mailer import StatementNotificationInfo, publish_payment_notification | ||
|
||
|
||
class StatementDueTask: | ||
|
@@ -47,18 +47,20 @@ def process_unpaid_statements(cls): | |
cls._notify_for_monthly() | ||
|
||
# Set overdue status for invoices | ||
if current_local_time().date().day == 1: | ||
cls._update_invoice_overdue_status() | ||
cls._update_invoice_overdue_status() | ||
|
||
@classmethod | ||
def _update_invoice_overdue_status(cls): | ||
"""Update the status of any invoices that are overdue.""" | ||
legislative_timezone = current_app.config.get('LEGISLATIVE_TIMEZONE') | ||
overdue_datetime = func.timezone(legislative_timezone, func.timezone('UTC', InvoiceModel.overdue_date)) | ||
|
||
unpaid_status = ( | ||
InvoiceStatus.SETTLEMENT_SCHEDULED.value, InvoiceStatus.PARTIAL.value, InvoiceStatus.CREATED.value) | ||
db.session.query(InvoiceModel)\ | ||
db.session.query(InvoiceModel) \ | ||
.filter(InvoiceModel.payment_method_code == PaymentMethod.EFT.value, | ||
InvoiceModel.overdue_date.isnot(None), | ||
InvoiceModel.overdue_date.cast(Date) <= current_local_time().date(), | ||
func.date(overdue_datetime) <= current_local_time().date(), | ||
InvoiceModel.invoice_status_code.in_(unpaid_status))\ | ||
.update({InvoiceModel.invoice_status_code: InvoiceStatus.OVERDUE.value}, synchronize_session='fetch') | ||
|
||
|
@@ -67,23 +69,28 @@ def _update_invoice_overdue_status(cls): | |
@classmethod | ||
def _notify_for_monthly(cls): | ||
"""Notify for unpaid monthly statements with an amount owing.""" | ||
# Check if we need to send a notification | ||
send_notification, is_due, last_day, previous_month = cls.determine_to_notify_and_is_due() | ||
|
||
if send_notification: | ||
statement_settings = StatementSettingsModel.find_accounts_settings_by_frequency(previous_month, | ||
StatementFrequency.MONTHLY) | ||
auth_account_ids = [pay_account.auth_account_id for _, pay_account in statement_settings] | ||
|
||
for account_id in auth_account_ids: | ||
try: | ||
# Get the most recent monthly statement | ||
statement = cls.find_most_recent_statement(account_id, StatementFrequency.MONTHLY.value) | ||
summary = Statement.get_summary(account_id, statement.id) | ||
payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(statement.payment_account_id) | ||
previous_month = current_local_time().replace(day=1) - timedelta(days=1) | ||
statement_settings = StatementSettingsModel.find_accounts_settings_by_frequency(previous_month, | ||
StatementFrequency.MONTHLY) | ||
|
||
# Get EFT auth account ids for statements | ||
auth_account_ids = [pay_account.auth_account_id for _, pay_account in statement_settings | ||
if pay_account.payment_method == PaymentMethod.EFT.value] | ||
|
||
current_app.logger.info(f'Processing {len(auth_account_ids)} EFT accounts for monthly reminders.') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some debug, and added some additional filtering for EFT payment method for auth_account_ids |
||
|
||
# Send payment notification if payment account is using EFT and there is an amount owing | ||
if payment_account.payment_method == PaymentMethod.EFT.value and summary['total_due'] > 0: | ||
for account_id in auth_account_ids: | ||
try: | ||
# Get the most recent monthly statement | ||
statement = cls.find_most_recent_statement(account_id, StatementFrequency.MONTHLY.value) | ||
invoices: [InvoiceModel] = StatementModel.find_all_payments_and_invoices_for_statement(statement.id) | ||
# check if there is an unpaid statement invoice that requires a reminder | ||
send_notification, is_due, due_date = cls.determine_to_notify_and_is_due(invoices) | ||
|
||
if send_notification: | ||
summary = Statement.get_summary(account_id, statement.id) | ||
# Send payment notification if there is an amount owing | ||
if summary['total_due'] > 0: | ||
recipients = StatementRecipientsModel. \ | ||
find_all_recipients_for_payment_id(statement.payment_account_id) | ||
|
||
|
@@ -94,45 +101,64 @@ def _notify_for_monthly(cls): | |
|
||
to_emails = ','.join([str(recipient.email) for recipient in recipients]) | ||
|
||
publish_payment_notification(pay_account=payment_account, | ||
statement=statement, | ||
is_due=is_due, | ||
due_date=last_day.date(), | ||
emails=to_emails) | ||
except Exception as e: # NOQA # pylint: disable=broad-except | ||
capture_message( | ||
f'Error on unpaid statement notification auth_account_id={account_id}, ' | ||
f'ERROR : {str(e)}', level='error') | ||
current_app.logger.error(e) | ||
continue | ||
publish_payment_notification( | ||
StatementNotificationInfo(auth_account_id=account_id, | ||
statement=statement, | ||
is_due=is_due, | ||
due_date=due_date, | ||
emails=to_emails, | ||
total_amount_owing=summary['total_due'])) | ||
except Exception as e: # NOQA # pylint: disable=broad-except | ||
capture_message( | ||
f'Error on unpaid statement notification auth_account_id={account_id}, ' | ||
f'ERROR : {str(e)}', level='error') | ||
current_app.logger.error(e) | ||
continue | ||
|
||
@classmethod | ||
def find_most_recent_statement(cls, auth_account_id: str, statement_frequency: str) -> StatementModel: | ||
"""Find all payment and invoices specific to a statement.""" | ||
query = db.session.query(StatementModel) \ | ||
.join(PaymentAccountModel, PaymentAccountModel.auth_account_id == auth_account_id) \ | ||
.join(PaymentAccountModel) \ | ||
.filter(PaymentAccountModel.auth_account_id == auth_account_id) \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed bad join, missing criteria |
||
.filter(StatementModel.frequency == statement_frequency) \ | ||
.order_by(StatementModel.to_date.desc()) | ||
|
||
return query.first() | ||
|
||
@classmethod | ||
def determine_to_notify_and_is_due(cls): | ||
def determine_to_notify_and_is_due(cls, invoices: [InvoiceModel]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored to be properly driven by overdue date |
||
"""Determine whether a statement notification is required and due.""" | ||
now = current_local_time() | ||
previous_month = now.replace(day=1) - timedelta(days=1) | ||
unpaid_status = [InvoiceStatus.SETTLEMENT_SCHEDULED.value, InvoiceStatus.PARTIAL.value, | ||
InvoiceStatus.CREATED.value] | ||
now = current_local_time().date() | ||
send_notification = False | ||
is_due = False | ||
|
||
# Send payment notification if it is 7 days before the due date or on the due date | ||
_, last_day = get_first_and_last_dates_of_month(now.month, now.year) | ||
if last_day.date() == now.date(): | ||
# Last day of the month, send payment due | ||
send_notification = True | ||
is_due = True | ||
elif now.date() == (last_day - timedelta(days=7)).date(): | ||
# 7 days from payment due date, send payment reminder | ||
send_notification = True | ||
is_due = False | ||
|
||
return send_notification, is_due, last_day, previous_month | ||
due_date = None | ||
|
||
invoice: InvoiceModel | ||
for invoice in invoices: | ||
if invoice.invoice_status_code not in unpaid_status or invoice.overdue_date is None: | ||
continue | ||
|
||
invoice_due_date = get_local_time(invoice.overdue_date) \ | ||
.date() - timedelta(days=1) # Day before invoice overdue date | ||
invoice_reminder_date = invoice_due_date - timedelta(days=7) # 7 days before invoice due date | ||
|
||
# Send payment notification if it is 7 days before the overdue date or on the overdue date | ||
if invoice_due_date == now: | ||
# due today, send payment due | ||
send_notification = True | ||
is_due = True | ||
due_date = invoice_due_date | ||
current_app.logger.info(f'Found invoice due: {invoice.id}.') | ||
break | ||
if invoice_reminder_date == now: | ||
# 7 days till due date, send payment reminder | ||
send_notification = True | ||
is_due = False | ||
due_date = invoice_due_date | ||
current_app.logger.info(f'Found invoice for 7 day reminder: {invoice.id}.') | ||
break | ||
|
||
return send_notification, is_due, due_date |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ | |
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Task to activate accounts with pending activation.Mostly for PAD with 3 day activation period.""" | ||
|
||
from dataclasses import dataclass | ||
from datetime import datetime | ||
from typing import Dict | ||
|
||
|
@@ -24,6 +24,18 @@ | |
from sentry_sdk import capture_message | ||
|
||
|
||
@dataclass | ||
class StatementNotificationInfo: | ||
"""Used for Statement Notifications.""" | ||
|
||
auth_account_id: str | ||
statement: StatementModel | ||
is_due: bool | ||
due_date: datetime | ||
emails: str | ||
total_amount_owing: float | ||
|
||
|
||
def publish_mailer_events(message_type: str, pay_account: PaymentAccountModel, | ||
additional_params: Dict = {}): | ||
"""Publish payment message to the mailer queue.""" | ||
|
@@ -72,7 +84,7 @@ def publish_statement_notification(pay_account: PaymentAccountModel, statement: | |
'emailAddresses': emails, | ||
'accountId': pay_account.auth_account_id, | ||
'fromDate': f'{statement.from_date}', | ||
'toDate:': f'{statement.to_date}', | ||
'toDate': f'{statement.to_date}', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed typo in params causing failure on the account mailer |
||
'statementFrequency': statement.frequency, | ||
'totalAmountOwing': total_amount_owing | ||
} | ||
|
@@ -94,24 +106,24 @@ def publish_statement_notification(pay_account: PaymentAccountModel, statement: | |
return True | ||
|
||
|
||
def publish_payment_notification(pay_account: PaymentAccountModel, statement: StatementModel, | ||
is_due: bool, due_date: datetime, emails: str) -> bool: | ||
def publish_payment_notification(info: StatementNotificationInfo) -> bool: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converted params to dataclass to make it cleaner |
||
"""Publish payment notification message to the mailer queue.""" | ||
notification_type = 'bc.registry.payment.statementDueNotification' if is_due \ | ||
notification_type = 'bc.registry.payment.statementDueNotification' if info.is_due \ | ||
else 'bc.registry.payment.statementReminderNotification' | ||
|
||
payload = { | ||
'specversion': '1.x-wip', | ||
'type': notification_type, | ||
'source': f'https://api.pay.bcregistry.gov.bc.ca/v1/accounts/{pay_account.auth_account_id}', | ||
'id': f'{pay_account.auth_account_id}', | ||
'source': f'https://api.pay.bcregistry.gov.bc.ca/v1/accounts/{info.auth_account_id}', | ||
'id': info.auth_account_id, | ||
'time': f'{datetime.now()}', | ||
'datacontenttype': 'application/json', | ||
'data': { | ||
'emailAddresses': emails, | ||
'accountId': pay_account.auth_account_id, | ||
'dueDate': f'{due_date}', | ||
'statementFrequency': statement.frequency | ||
'emailAddresses': info.emails, | ||
'accountId': info.auth_account_id, | ||
'dueDate': f'{info.due_date}', | ||
'statementFrequency': info.statement.frequency, | ||
'totalAmountOwing': info.total_amount_owing | ||
} | ||
} | ||
try: | ||
|
@@ -121,10 +133,10 @@ def publish_payment_notification(pay_account: PaymentAccountModel, statement: St | |
except Exception as e: # pylint: disable=broad-except | ||
current_app.logger.error(e) | ||
current_app.logger.warning('Notification to Queue failed for the Account Mailer %s - %s', | ||
pay_account.auth_account_id, | ||
info.auth_account_id, | ||
payload) | ||
capture_message('Notification to Queue failed for the Account Mailer {auth_account_id}, {msg}.'.format( | ||
auth_account_id=pay_account.auth_account_id, msg=payload), level='error') | ||
auth_account_id=info.auth_account_id, msg=payload), level='error') | ||
|
||
return False | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,11 @@ def apply_credit(self, invoice: Invoice) -> None: | |
self.create_receipt(invoice=invoice_model, payment=payment).save() | ||
self._release_payment(invoice=invoice) | ||
|
||
def complete_post_invoice(self, invoice: Invoice, invoice_reference: InvoiceReference) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accidentally reverted this change in a previous PR, adding back in. This allow payments to be properly released for EFT when invoices are created. |
||
"""Complete any post invoice activities if needed.""" | ||
# Publish message to the queue with payment token, so that they can release records on their side. | ||
self._release_payment(invoice=invoice) | ||
|
||
def create_payment(self, payment_account: PaymentAccountModel, invoice: InvoiceModel, payment_date: datetime, | ||
paid_amount) -> PaymentModel: | ||
"""Create a payment record for an invoice.""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix timezone issue