From 8d1ee7ebed53d1c87ec84839201b401a8fa17a35 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Fri, 17 Oct 2025 00:12:05 -0600 Subject: [PATCH 1/4] Revise report window calculation --- .../common/cc_common/email_service_client.py | 14 +- .../handlers/transaction_reporting.py | 142 +++----- .../lambdas/python/purchases/report_window.py | 141 ++++++++ .../test_transaction_reporting.py | 327 ++++++++++-------- .../tests/unit/test_data_model/__init__.py | 0 .../tests/unit/test_report_window.py | 214 ++++++++++++ 6 files changed, 589 insertions(+), 249 deletions(-) create mode 100644 backend/compact-connect/lambdas/python/purchases/report_window.py delete mode 100644 backend/compact-connect/lambdas/python/purchases/tests/unit/test_data_model/__init__.py create mode 100644 backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py diff --git a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py index 23e3039a9..410a8e764 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py @@ -1,6 +1,6 @@ import json from dataclasses import dataclass -from datetime import date, datetime +from datetime import date from typing import Any, Protocol from uuid import UUID @@ -144,8 +144,8 @@ def send_compact_transaction_report_email( compact: str, report_s3_path: str, reporting_cycle: str, - start_date: datetime, - end_date: datetime, + start_date: date, + end_date: date, ) -> dict[str, Any]: """ Send a compact transaction report email. @@ -153,8 +153,8 @@ def send_compact_transaction_report_email( :param compact: Compact name :param report_s3_path: S3 path to the report zip file :param reporting_cycle: Reporting cycle (e.g., 'weekly', 'monthly') - :param start_date: Start datetime of the reporting period - :param end_date: End datetime of the reporting period + :param start_date: Start date of the reporting period + :param end_date: End date of the reporting period :return: Response from the email notification service """ @@ -178,8 +178,8 @@ def send_jurisdiction_transaction_report_email( jurisdiction: str, report_s3_path: str, reporting_cycle: str, - start_date: datetime, - end_date: datetime, + start_date: date, + end_date: date, ) -> dict[str, str]: """ Send a jurisdiction transaction report email. diff --git a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py index 2b559fab0..0d91978bd 100644 --- a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py +++ b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py @@ -1,7 +1,7 @@ from __future__ import annotations import csv -from datetime import datetime, timedelta +from datetime import date, datetime from decimal import Decimal from enum import StrEnum from io import BytesIO, StringIO @@ -13,6 +13,7 @@ from cc_common.data_model.schema.compact.common import COMPACT_TYPE from cc_common.data_model.schema.jurisdiction.common import JURISDICTION_TYPE from cc_common.exceptions import CCInternalException +from lambdas.python.purchases.report_window import ReportCycle, ReportWindow class ReportableTransactionStatuses(StrEnum): @@ -21,73 +22,10 @@ class ReportableTransactionStatuses(StrEnum): SettledSuccessfully = 'settledSuccessfully' -def _get_display_date_range(reporting_cycle: str) -> tuple[datetime, datetime]: - """Get the display date range for reports. - - These dates are used for report filenames and email notifications. - - :param reporting_cycle: Either 'weekly' or 'monthly' - :return: Tuple of (start_time, end_time) in UTC for display purposes - """ - if reporting_cycle == 'weekly': - end_time = config.current_standard_datetime - # Go back 7 days to capture the full week - start_time = end_time - timedelta(days=7) - return start_time, end_time - if reporting_cycle == 'monthly': - # Reports run on the first day of the month. - # Knowing this, we can use the current date to get the start and end of the month. - # By going back 1 day from the first day of the current month, we get the last day of the previous month. - end_time = config.current_standard_datetime.replace( - day=1, hour=0, minute=0, second=0, microsecond=0 - ) - timedelta(days=1) - # Start time is the first day of the previous month - start_time = end_time.replace(day=1) - return start_time, end_time - raise ValueError(f'Invalid reporting cycle: {reporting_cycle}') - - -def _get_query_date_range(reporting_cycle: str) -> tuple[datetime, datetime]: - """Get the query date range for DynamoDB queries. - - Our Sort Key format for transactions includes additional components after the timestamp - (COMPACT#name#TIME#timestamp#BATCH#id#TX#id), So the DynamoDB BETWEEN condition is INCLUSIVE for the beginning - range and EXCLUSIVE at the end range. This is because DynamoDB performs lexicographical comparison on the entire - sort key string. When the sort key continues beyond the comparison value: - - - For the lower bound: Additional characters after the comparison point make the full key "greater than" the bound, - satisfying the >= condition - - For the upper bound: Additional characters after the comparison point make the full key "greater than" the bound, - failing the <= condition - - We need to adjust our timestamps accordingly to ensure we capture all settled transactions exactly once. - - :param reporting_cycle: Either 'weekly' or 'monthly' - :return: Tuple of (start_time, end_time) in UTC for DynamoDB queries - """ - if reporting_cycle == 'weekly': - # Reports run on Friday 10:00 PM UTC - end_time = config.current_standard_datetime.replace(hour=22, minute=0, second=0, microsecond=0) - # Go back 7 days to capture the full week - start_time = end_time - timedelta(days=7) - return start_time, end_time - - if reporting_cycle == 'monthly': - # Reports run on the first day of the month - # End time is midnight, since that will be excluded from the BETWEEN key condition - end_time = config.current_standard_datetime.replace(hour=0, minute=0, second=0, microsecond=0) - # Start time is midnight of the previous month - start_time = (end_time - timedelta(days=1)).replace(day=1, hour=0, minute=0, second=0, microsecond=0) - return start_time, end_time - - raise ValueError(f'Invalid reporting cycle: {reporting_cycle}') - - def _store_compact_reports_in_s3( compact: str, reporting_cycle: str, - start_time: datetime, - end_time: datetime, + report_window: ReportWindow, summary_report: str, transaction_detail: str, bucket_name: str, @@ -96,24 +34,22 @@ def _store_compact_reports_in_s3( :param compact: Compact name :param reporting_cycle: Either 'weekly' or 'monthly' - :param start_time: Report start time - :param end_time: Report end time + :param report_window: the Report Window :param summary_report: Financial summary report CSV content :param transaction_detail: Transaction detail report CSV content :param bucket_name: S3 bucket name :return: Dictionary of file types to their S3 paths """ - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' base_path = ( f'compact/{compact}/reports/compact-transactions/reporting-cycle/{reporting_cycle}/' - f'{end_time.strftime("%Y/%m/%d")}' + f'{report_window.display_end.strftime("%Y/%m/%d")}' ) # Define paths for all report files # Currently, we are only sending the .zip file in the email reporting, but there is potential # to store .gz files in the future paths = { - 'report_zip': f'{base_path}/{compact}-{date_range}-report.zip', + 'report_zip': f'{base_path}/{compact}-{report_window.display_text}-report.zip', } s3_client = config.s3_client @@ -121,8 +57,8 @@ def _store_compact_reports_in_s3( # Create and store combined zip with uncompressed CSVs zip_buffer = BytesIO() with ZipFile(zip_buffer, 'w', compression=ZIP_DEFLATED) as zip_file: - zip_file.writestr(f'financial-summary-{date_range}.csv', summary_report.encode('utf-8')) - zip_file.writestr(f'transaction-detail-{date_range}.csv', transaction_detail.encode('utf-8')) + zip_file.writestr(f'financial-summary-{report_window.display_text}.csv', summary_report.encode('utf-8')) + zip_file.writestr(f'transaction-detail-{report_window.display_text}.csv', transaction_detail.encode('utf-8')) s3_client.put_object(Bucket=bucket_name, Key=paths['report_zip'], Body=zip_buffer.getvalue()) return paths @@ -132,8 +68,7 @@ def _store_jurisdiction_reports_in_s3( compact: str, jurisdiction: str, reporting_cycle: str, - start_time: datetime, - end_time: datetime, + report_window: ReportWindow, transaction_detail: str, bucket_name: str, ) -> dict[str, str]: @@ -142,23 +77,21 @@ def _store_jurisdiction_reports_in_s3( :param compact: Compact name :param jurisdiction: Jurisdiction postal code :param reporting_cycle: Either 'weekly' or 'monthly' - :param start_time: Report start time - :param end_time: Report end time + :param report_window: The report window :param transaction_detail: Transaction detail report CSV content :param bucket_name: S3 bucket name :return: Dictionary of file types to their S3 paths """ - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' base_path = ( f'compact/{compact}/reports/jurisdiction-transactions/jurisdiction/{jurisdiction}/' - f'reporting-cycle/{reporting_cycle}/{end_time.strftime("%Y/%m/%d")}' + f'reporting-cycle/{reporting_cycle}/{report_window.display_end.strftime("%Y/%m/%d")}' ) # Define paths for all report files # Currently, we are only sending the .zip file in the email reporting, but there is potential # to store .gz files in the future paths = { - 'report_zip': f'{base_path}/{jurisdiction}-{date_range}-report.zip', + 'report_zip': f'{base_path}/{jurisdiction}-{report_window.display_text}-report.zip', } s3_client = config.s3_client @@ -166,7 +99,9 @@ def _store_jurisdiction_reports_in_s3( # Create and store zip with uncompressed CSV zip_buffer = BytesIO() with ZipFile(zip_buffer, 'w', compression=ZIP_DEFLATED) as zip_file: - zip_file.writestr(f'{jurisdiction}-transaction-detail-{date_range}.csv', transaction_detail.encode('utf-8')) + zip_file.writestr( + f'{jurisdiction}-transaction-detail-{report_window.display_text}.csv', transaction_detail.encode('utf-8') + ) s3_client.put_object(Bucket=bucket_name, Key=paths['report_zip'], Body=zip_buffer.getvalue()) return paths @@ -186,8 +121,26 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: :return: Success message """ compact = event['compact'] - reporting_cycle = event['reportingCycle'] - logger.info('Generating transaction reports', compact=compact, reporting_cycle=reporting_cycle) + reporting_cycle = ReportCycle(event['reportingCycle']) + + # Support 'manual' report date overrides for re-runs + report_start_override = event.get('reportStartOverride') + report_end_override = event.get('reportEndOverride') + if report_start_override and report_end_override: + report_window = ReportWindow( + reporting_cycle, + _display_start_date=date.fromisoformat(report_start_override), + _display_end_date=date.fromisoformat(report_end_override), + ) + else: + report_window = ReportWindow(reporting_cycle) + + logger.info( + 'Generating transaction reports', + compact=compact, + reporting_cycle=reporting_cycle, + window=report_window.display_text, + ) # this is used to track any errors that occur when generating the reports # without preventing valid reports from being sent @@ -216,16 +169,9 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: # Get the S3 bucket name bucket_name = config.transaction_reports_bucket_name - # Get both query and display date ranges - query_start_time, query_end_time = _get_query_date_range(reporting_cycle) - - # Convert query times to epochs for DynamoDB - start_epoch = int(query_start_time.timestamp()) - end_epoch = int(query_end_time.timestamp()) - # Get all transactions for the time period transactions = transaction_client.get_transactions_in_range( - compact=compact, start_epoch=start_epoch, end_epoch=end_epoch + compact=compact, start_epoch=report_window.start_epoch, end_epoch=report_window.end_epoch ) # For now, we only report on transactions that have been successfully settled, so we filter to only include @@ -263,14 +209,11 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: compact_transaction_csv = _generate_compact_transaction_report(transactions, providers) jurisdiction_reports = _generate_jurisdiction_reports(transactions, providers, jurisdiction_configurations) - display_start_time, display_end_time = _get_display_date_range(reporting_cycle) - # Store compact reports in S3 and get paths compact_paths = _store_compact_reports_in_s3( compact=compact, reporting_cycle=reporting_cycle, - start_time=display_start_time, - end_time=display_end_time, + report_window=report_window, summary_report=compact_summary_csv, transaction_detail=compact_transaction_csv, bucket_name=bucket_name, @@ -282,8 +225,8 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: compact=compact, report_s3_path=compact_paths['report_zip'], reporting_cycle=reporting_cycle, - start_date=display_start_time, - end_date=display_end_time, + start_date=report_window.display_start, + end_date=report_window.display_end, ) except CCInternalException as e: logger.error( @@ -300,8 +243,7 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: compact=compact, jurisdiction=jurisdiction, reporting_cycle=reporting_cycle, - start_time=display_start_time, - end_time=display_end_time, + report_window=report_window, transaction_detail=report_csv, bucket_name=bucket_name, ) @@ -312,8 +254,8 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: jurisdiction=jurisdiction, report_s3_path=jurisdiction_paths['report_zip'], reporting_cycle=reporting_cycle, - start_date=display_start_time, - end_date=display_end_time, + start_date=report_window.display_start, + end_date=report_window.display_end, ) except CCInternalException as e: logger.error( diff --git a/backend/compact-connect/lambdas/python/purchases/report_window.py b/backend/compact-connect/lambdas/python/purchases/report_window.py new file mode 100644 index 000000000..b529560ab --- /dev/null +++ b/backend/compact-connect/lambdas/python/purchases/report_window.py @@ -0,0 +1,141 @@ +from datetime import UTC, date, datetime, time, timedelta +from enum import StrEnum + +from cc_common.config import config + + +class ReportCycle(StrEnum): + """Report cycles.""" + + WEEKLY = 'weekly' + MONTHLY = 'monthly' + + +class ReportWindow: + """ + Manages reporting window start/end times for both query and display + + All windows start and end at midnight. + """ + + def __init__(self, report_cycle: ReportCycle, *, _display_start_date: date = None, _display_end_date: date = None): + """ + :param report_cycle: The ReportCycle this report will run for (weekly or monthly) + :param _display_start_date: Optional override of start date. Required with _display_end_date. + :param _display_end_date: Optional override of end date. Required with _display_start_date. + """ + super().__init__() + self._report_cycle = report_cycle + if _display_start_date and _display_end_date: + self._start_time = datetime.combine( + _display_start_date, time(hour=0, minute=0, second=0, microsecond=0, tzinfo=UTC) + ) + self._end_time = datetime.combine( + # Add 1 to convert display day to query datetime + _display_end_date + timedelta(days=1), + time(hour=0, minute=0, second=0, microsecond=0, tzinfo=UTC), + ) + else: + self._start_time, self._end_time = self._get_query_date_range() + + @property + def query_window(self) -> tuple[datetime, datetime]: + """Get the query window for DynamoDB queries. + + :return: (start_time, end_time) + """ + return self._start_time, self._end_time + + @property + def start_epoch(self) -> int: + """ + The POSIX timestamp marking the beginning (inclusive) of the reporting window + """ + return int(self._start_time.timestamp()) + + @property + def end_epoch(self) -> int: + """ + The POSIX timestamp marking the end (exclusive) of the reporting window + """ + return int(self._end_time.timestamp()) + + @property + def display_start(self) -> date: + return self._start_time.date() + + @property + def display_end(self) -> date: + return (self._end_time - timedelta(seconds=1)).date() + + @property + def display_window(self) -> tuple[date, date]: + """Get the display window for report dates. + + :return: (start_date, end_date) + """ + return self.display_start, self.display_end + + @property + def display_start_text(self) -> str: + return self.display_start.strftime('%Y-%m-%d') + + @property + def display_end_text(self) -> str: + return self.display_end.strftime('%Y-%m-%d') + + @property + def display_text(self) -> str: + return f'{self.display_start_text}--{self.display_end_text}' + + def _get_query_date_range(self) -> tuple[datetime, datetime]: + """Get the query date range for DynamoDB queries. + + :return: (start_time, end_time) + + Our Sort Key format for transactions includes additional components after the timestamp + (COMPACT#name#TIME#timestamp#BATCH#id#TX#id), So the DynamoDB BETWEEN condition is INCLUSIVE for the beginning + range and EXCLUSIVE at the end range. This is because DynamoDB performs lexicographical comparison on the entire + sort key string. When the sort key continues beyond the comparison value: + + - For the lower bound: Additional characters after the comparison point make the full key "greater than" the + bound, satisfying the >= condition + - For the upper bound: Additional characters after the comparison point make the full key "greater than" the + bound, failing the <= condition + + We need to adjust our timestamps accordingly to ensure we capture all settled transactions exactly once. + + :return: Tuple of (start_time, end_time) for DynamoDB queries + """ + if self._report_cycle == ReportCycle.WEEKLY: + # Reports exclude anything before the most recent Midnight on Saturday + end_time = self._get_most_recent_saturday_midnight(config.current_standard_datetime) + # Go back 7 days to capture the full week + start_time = end_time - timedelta(days=7) + return start_time, end_time + + if self._report_cycle == ReportCycle.MONTHLY: + # Reports run on the first day of the month + # End time is midnight, since that will be excluded from the BETWEEN key condition + end_time = config.current_standard_datetime.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + # Start time is midnight of the previous month + start_time = (end_time - timedelta(days=1)).replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return start_time, end_time + + raise ValueError(f'Invalid report cycle: {self._report_cycle}') + + @staticmethod + def _get_most_recent_saturday_midnight(now: datetime) -> datetime: + """ + Returns the most recent Saturday midnight (00:00:00) + given a timezone-aware datetime object. + """ + # weekday() returns 0=Monday, 1=Tuesday, ..., 5=Saturday, 6=Sunday + days_since_saturday = (now.weekday() - 5) % 7 + + # If it's currently Saturday, days_since_saturday is 0 + # If it's Sunday, days_since_saturday is 1, etc. + saturday = now - timedelta(days=days_since_saturday) + + # Replace time with midnight, keeping the timezone info + return saturday.replace(hour=0, minute=0, second=0, microsecond=0) diff --git a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py index 2fc718a3d..104ce7933 100644 --- a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py +++ b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py @@ -1,6 +1,6 @@ # ruff: noqa: E501 line-too-long The lines displaying the csv file contents are long, but they are necessary for the test. import json -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from decimal import Decimal from io import BytesIO from unittest.mock import call, patch @@ -244,33 +244,30 @@ def _add_compact_configuration_data(self, jurisdictions=None): record.update(jurisdiction) self._compact_configuration_table.put_item(Item=record) - def _validate_compact_email_notification(self, mock_email_service_client, reporting_cycle, start_time, end_time): - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + def _validate_compact_email_notification(self, mock_email_service_client, reporting_cycle, report_window): # Check compact report email notification expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/{reporting_cycle}/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ) mock_email_service_client.send_compact_transaction_report_email.assert_called_once_with( compact=TEST_COMPACT, report_s3_path=expected_compact_path, reporting_cycle=reporting_cycle, - start_date=start_time, - end_date=end_time, + start_date=report_window.display_start, + end_date=report_window.display_end, ) return expected_compact_path def _validate_jurisdiction_email_notification( - self, mock_email_service_client, jurisdiction, reporting_cycle, start_time, end_time + self, mock_email_service_client, jurisdiction, reporting_cycle, report_window ): - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' - expected_jurisdiction_path = ( f'compact/{TEST_COMPACT}/reports/jurisdiction-transactions/jurisdiction/{jurisdiction}/' - f'reporting-cycle/{reporting_cycle}/{end_time.strftime("%Y/%m/%d")}/' - f'{jurisdiction}-{date_range}-report.zip' + f'reporting-cycle/{reporting_cycle}/{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{jurisdiction}-{report_window.display_text}-report.zip' ) email_service_client_calls = mock_email_service_client.send_jurisdiction_transaction_report_email.call_args_list expected_call = call( @@ -278,8 +275,8 @@ def _validate_jurisdiction_email_notification( jurisdiction=jurisdiction, report_s3_path=expected_jurisdiction_path, reporting_cycle=reporting_cycle, - start_date=start_time, - end_date=end_time, + start_date=report_window.display_start, + end_date=report_window.display_end, ) self.assertIn(expected_call, email_service_client_calls) @@ -293,26 +290,26 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac ): """Test successful processing of settled transactions.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) self._add_compact_configuration_data([OHIO_JURISDICTION]) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) # Generate the reports generate_transaction_reports(generate_mock_event(), self.mock_context) expected_compact_path = self._validate_compact_email_notification( mock_email_service_client=mock_email_service_client, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Verify S3 stored files @@ -323,7 +320,7 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Ohio,0\n' @@ -335,7 +332,7 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac ) # Check transaction detail - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') self.assertEqual( 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status\n' @@ -347,9 +344,8 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac expected_ohio_path = self._validate_jurisdiction_email_notification( mock_email_service_client=mock_email_service_client, jurisdiction='oh', - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Check jurisdiction report @@ -358,7 +354,7 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac ) with ZipFile(BytesIO(ohio_zip_obj['Body'].read())) as zip_file: - with zip_file.open(f'oh-transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'oh-transaction-detail-{report_window.display_text}.csv') as f: ohio_content = f.read().decode('utf-8') self.assertEqual( 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State Fee,State,Transaction Id,Privilege Id,Transaction Status\n' @@ -369,12 +365,52 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac ohio_content, ) + # event bridge triggers the weekly report at Friday 10:00 PM UTC (5:00 PM EST) + @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-05T22:00:00+00:00')) + @patch('handlers.transaction_reporting.config.email_service_client') + def test_generate_transaction_reports_supports_date_overrides(self, mock_email_service_client): + """Test successful processing of settled transactions.""" + from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow + + _set_default_email_service_client_behavior(mock_email_service_client) + + self._add_compact_configuration_data([OHIO_JURISDICTION]) + + # Generate the reports + event = generate_mock_event() + # A previous weekly report + event['reportStartOverride'] = '2025-03-22' + event['reportEndOverride'] = '2025-03-28' + generate_transaction_reports(event, self.mock_context) + + # Calculate expected date range + # the end time should be Friday more than a week back + end_date = date.fromisoformat('2025-03-28') + # the start time should be the prior Saturday + start_date = date.fromisoformat('2025-03-22') + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + + # Just check the expected emails were 'sent' with dates older than default + self._validate_compact_email_notification( + mock_email_service_client=mock_email_service_client, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, + ) + self._validate_jurisdiction_email_notification( + mock_email_service_client=mock_email_service_client, + jurisdiction='oh', + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, + ) + # event bridge triggers the weekly report at Friday 10:00 PM UTC (5:00 PM EST) @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2025-04-05T22:00:00+00:00')) @patch('handlers.transaction_reporting.config.email_service_client') def test_generate_report_collects_transactions_across_two_months(self, mock_email_service_client): """Test successful processing of settled transactions.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -400,20 +436,19 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) # Verify email notifications using the new pattern expected_compact_path = self._validate_compact_email_notification( mock_email_service_client=mock_email_service_client, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Check jurisdiction report emails @@ -421,9 +456,8 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai self._validate_jurisdiction_email_notification( mock_email_service_client=mock_email_service_client, jurisdiction=jurisdiction, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Verify S3 stored files for compact report @@ -433,7 +467,7 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -447,7 +481,7 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai ) # Check transaction detail - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') self.assertEqual( f'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status\n' @@ -462,13 +496,13 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai Bucket=self.config.transaction_reports_bucket_name, Key=( f'compact/{TEST_COMPACT}/reports/jurisdiction-transactions/jurisdiction/{jurisdiction}/' - f'reporting-cycle/weekly/{end_time.strftime("%Y/%m/%d")}/' - f'{jurisdiction}-{date_range}-report.zip' + f'reporting-cycle/weekly/{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{jurisdiction}-{report_window.display_text}-report.zip' ), ) with ZipFile(BytesIO(jurisdiction_zip_obj['Body'].read())) as zip_file: - with zip_file.open(f'{jurisdiction}-transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'{jurisdiction}-transaction-detail-{report_window.display_text}.csv') as f: content = f.read().decode('utf-8') transaction_date = '03-30-2025' if jurisdiction == 'oh' else '04-01-2025' self.assertEqual( @@ -486,6 +520,7 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai def test_generate_report_with_multiple_privileges_in_single_transaction(self, mock_email_service_client): """Test processing of transactions with multiple privileges in a single transaction.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -503,20 +538,19 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) # Verify compact email notification expected_compact_path = self._validate_compact_email_notification( mock_email_service_client=mock_email_service_client, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Verify S3 stored files for compact report @@ -526,7 +560,7 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -542,7 +576,7 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo ) # Check transaction detail - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') expected_lines = [ 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status' @@ -558,9 +592,8 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo expected_jurisdiction_path = self._validate_jurisdiction_email_notification( mock_email_service_client=mock_email_service_client, jurisdiction=jurisdiction, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) jurisdiction_zip_obj = self.config.s3_client.get_object( Bucket=self.config.transaction_reports_bucket_name, @@ -568,7 +601,7 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo ) with ZipFile(BytesIO(jurisdiction_zip_obj['Body'].read())) as zip_file: - with zip_file.open(f'{jurisdiction}-transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'{jurisdiction}-transaction-detail-{report_window.display_text}.csv') as f: content = f.read().decode('utf-8') self.assertEqual( 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State Fee,State,Transaction Id,Privilege Id,Transaction Status\n' @@ -585,6 +618,7 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo def test_generate_report_with_large_number_of_transactions_and_providers(self, mock_email_service_client): """Test processing of a large number of transactions (>500) and providers (>100).""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -610,11 +644,11 @@ def test_generate_report_with_large_number_of_transactions_and_providers(self, m ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -624,14 +658,14 @@ def test_generate_report_with_large_number_of_transactions_and_providers(self, m Bucket=self.config.transaction_reports_bucket_name, Key=( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ), ) with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,300\n' @@ -645,7 +679,7 @@ def test_generate_report_with_large_number_of_transactions_and_providers(self, m ) # Check transaction detail - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8').split('\n') # Verify header self.assertEqual( @@ -676,13 +710,13 @@ def test_generate_report_with_large_number_of_transactions_and_providers(self, m Bucket=self.config.transaction_reports_bucket_name, Key=( f'compact/{TEST_COMPACT}/reports/jurisdiction-transactions/jurisdiction/{jurisdiction}/' - f'reporting-cycle/weekly/{end_time.strftime("%Y/%m/%d")}/' - f'{jurisdiction}-{date_range}-report.zip' + f'reporting-cycle/weekly/{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{jurisdiction}-{report_window.display_text}-report.zip' ), ) with ZipFile(BytesIO(jurisdiction_zip_obj['Body'].read())) as zip_file: - with zip_file.open(f'{jurisdiction}-transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'{jurisdiction}-transaction-detail-{report_window.display_text}.csv') as f: content = f.read().decode('utf-8').split('\n') # Verify header @@ -736,15 +770,16 @@ def test_generate_report_handles_unknown_jurisdiction(self, mock_email_service_c This is unlikely to happen in practice, but we should handle it gracefully. """ from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) self._add_compact_configuration_data(jurisdictions=[OHIO_JURISDICTION, KENTUCKY_JURISDICTION]) @@ -768,14 +803,14 @@ def test_generate_report_handles_unknown_jurisdiction(self, mock_email_service_c Bucket=self.config.transaction_reports_bucket_name, Key=( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ), ) with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') # Verify compact summary includes unknown jurisdiction self.assertEqual( @@ -816,6 +851,7 @@ def test_generate_monthly_report_includes_expected_settled_transactions_for_full ): """Test processing monthly report with full month range for Feb 2024 (leap year).""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -862,19 +898,20 @@ def test_generate_monthly_report_includes_expected_settled_transactions_for_full # Calculate expected date range # the display end time should be the last day of the month - display_end_time = datetime.fromisoformat('2024-02-29T00:00:00+00:00') - # the start time should be the first day of the month - display_start_time = datetime.fromisoformat('2024-02-01T00:00:00+00:00') - date_range = f'{display_start_time.strftime("%Y-%m-%d")}--{display_end_time.strftime("%Y-%m-%d")}' + display_end = date.fromisoformat('2024-02-29') + # the start time should be the first day of the montn + display_start = date.fromisoformat('2024-02-01') + report_window = ReportWindow( + ReportCycle.WEEKLY, _display_start_date=display_start, _display_end_date=display_end + ) - generate_transaction_reports(generate_mock_event(reporting_cycle='monthly'), self.mock_context) + generate_transaction_reports(generate_mock_event(reporting_cycle=ReportCycle.MONTHLY), self.mock_context) # Verify email notifications using the new pattern expected_compact_path = self._validate_compact_email_notification( mock_email_service_client=mock_email_service_client, - reporting_cycle='monthly', - start_time=display_start_time, - end_time=display_end_time, + reporting_cycle=ReportCycle.MONTHLY, + report_window=report_window, ) # Verify S3 stored files for compact report @@ -884,7 +921,7 @@ def test_generate_monthly_report_includes_expected_settled_transactions_for_full with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -907,6 +944,7 @@ def test_generate_weekly_report_includes_expected_settled_transactions_for_full_ ): """Test processing weekly report with full week range for Mar 2024.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -916,65 +954,64 @@ def test_generate_weekly_report_includes_expected_settled_transactions_for_full_ mock_user = self._add_mock_provider_to_db('12345', 'John', 'Doe') - # Create a transaction with a privilege which is settled the first day of the week a second after 10:00 PM UTC + # Create a transaction with a privilege which is settled Saturday of the report week a second after midnight UTC self._add_mock_transaction_to_db( jurisdictions=['oh'], licensee_id=mock_user['providerId'], month_iso_string='2025-03', - transaction_settlement_time_utc=datetime.fromisoformat('2025-03-01T22:00:01+00:00'), + transaction_settlement_time_utc=datetime.fromisoformat('2025-03-01T00:00:01+00:00'), ) - # Create a transaction with a privilege which is settled the first day of the week right at 10:00 PM UTC + # Create a transaction with a privilege which is settled Saturday of the report week right at midnight UTC # This transaction should be included in the weekly report self._add_mock_transaction_to_db( jurisdictions=['ky'], licensee_id=mock_user['providerId'], month_iso_string='2025-03', - transaction_settlement_time_utc=datetime.fromisoformat('2025-03-01T22:00:00+00:00'), + transaction_settlement_time_utc=datetime.fromisoformat('2025-03-01T00:00:00+00:00'), ) - # Create a transaction with a privilege which is settled the last day of the week right at 9:59:59 PM UTC + # Create a transaction with a privilege which is settled Friday of the report week right at 11:59:59 PM UTC # This transaction should be included in the weekly report self._add_mock_transaction_to_db( jurisdictions=['ne'], licensee_id=mock_user['providerId'], month_iso_string='2025-03', - transaction_settlement_time_utc=datetime.fromisoformat('2025-03-08T21:59:59+00:00'), + transaction_settlement_time_utc=datetime.fromisoformat('2025-03-07T23:59:59+00:00'), ) - # Create a transaction with a privilege which is settled at the end of the week at 10:00 PM UTC + # Create a transaction with a privilege which is settled Saturday, midnight, just after the report week # This transaction should NOT be included in the weekly report self._add_mock_transaction_to_db( jurisdictions=['ky'], licensee_id=mock_user['providerId'], month_iso_string='2025-03', - transaction_settlement_time_utc=datetime.fromisoformat('2025-03-08T22:00:00+00:00'), + transaction_settlement_time_utc=datetime.fromisoformat('2025-03-08T00:00:00+00:00'), ) - # Create a transaction with a privilege which is settled the last day of the week a second after 10:00 PM UTC + # Create a transaction with a privilege which is settled Saturday, a second after midnight, just after the week # This transaction should NOT be included in the weekly report self._add_mock_transaction_to_db( jurisdictions=['ne'], licensee_id=mock_user['providerId'], month_iso_string='2025-03', - transaction_settlement_time_utc=datetime.fromisoformat('2025-03-08T22:00:01+00:00'), + transaction_settlement_time_utc=datetime.fromisoformat('2025-03-08T00:00:01+00:00'), ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-03-08T22:00:01+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-03-07') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) - generate_transaction_reports(generate_mock_event(reporting_cycle='weekly'), self.mock_context) + generate_transaction_reports(generate_mock_event(reporting_cycle=ReportCycle.WEEKLY), self.mock_context) # Verify email notifications using the new pattern expected_compact_path = self._validate_compact_email_notification( mock_email_service_client=mock_email_service_client, - reporting_cycle='weekly', - start_time=start_time, - end_time=end_time, + reporting_cycle=ReportCycle.WEEKLY, + report_window=report_window, ) # Verify S3 stored files for compact report @@ -984,7 +1021,7 @@ def test_generate_weekly_report_includes_expected_settled_transactions_for_full_ with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -1005,6 +1042,7 @@ def test_generate_weekly_report_includes_expected_settled_transactions_for_full_ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service_client): """Test processing of transactions with multiple privileges in a single transaction.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -1023,18 +1061,18 @@ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end time should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ) # Verify S3 stored files @@ -1045,7 +1083,7 @@ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -1062,7 +1100,7 @@ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service ) # Check transaction detail - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') expected_lines = [ 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status' @@ -1079,6 +1117,7 @@ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_service_client): """Test processing of transactions with multiple privileges in a single transaction.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -1098,11 +1137,11 @@ def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_se ) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end date should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) with self.assertRaises(CCInternalException) as exc_info: generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -1118,8 +1157,8 @@ def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_se expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ) # Verify S3 stored files @@ -1130,7 +1169,7 @@ def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_se with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -1153,6 +1192,7 @@ def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_se def test_generate_report_does_not_include_transactions_with_settlement_errors(self, mock_email_service_client): """Test that transactions with settlement errors are not included in the report.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -1180,15 +1220,16 @@ def test_generate_report_does_not_include_transactions_with_settlement_errors(se generate_transaction_reports(generate_mock_event(), self.mock_context) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end date should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ) # Verify S3 stored files @@ -1199,7 +1240,7 @@ def test_generate_report_does_not_include_transactions_with_settlement_errors(se with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary, which in this case should only include the successful transaction - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -1213,7 +1254,7 @@ def test_generate_report_does_not_include_transactions_with_settlement_errors(se ) # Check transaction detail, which in this case should only include the successful transaction - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') self.assertEqual( 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status\n' @@ -1227,6 +1268,7 @@ def test_generate_report_does_not_include_transactions_with_settlement_errors(se def test_generate_report_does_not_include_transactions_with_declined_status(self, mock_email_service_client): """Test that transactions with declined status are not included in the report.""" from handlers.transaction_reporting import generate_transaction_reports + from report_window import ReportCycle, ReportWindow _set_default_email_service_client_behavior(mock_email_service_client) @@ -1254,15 +1296,16 @@ def test_generate_report_does_not_include_transactions_with_declined_status(self generate_transaction_reports(generate_mock_event(), self.mock_context) # Calculate expected date range - # the end time should be Friday at 10:00 PM UTC - end_time = datetime.fromisoformat('2025-04-05T22:00:00+00:00') - # the start time should be 7 days ago at 10:00 PM UTC - start_time = end_time - timedelta(days=7) - date_range = f'{start_time.strftime("%Y-%m-%d")}--{end_time.strftime("%Y-%m-%d")}' + # the end date should be Friday + end_date = date.fromisoformat('2025-04-04') + # the start time should be the prior Saturday + start_date = end_date - timedelta(days=6) + report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' - f'{end_time.strftime("%Y/%m/%d")}/' - f'{TEST_COMPACT}-{date_range}-report.zip' + f'{report_window.display_end.strftime("%Y/%m/%d")}/' + f'{TEST_COMPACT}-{report_window.display_text}-report.zip' ) # Verify S3 stored files @@ -1273,7 +1316,7 @@ def test_generate_report_does_not_include_transactions_with_declined_status(self with ZipFile(BytesIO(compact_zip_obj['Body'].read())) as zip_file: # Check financial summary, which in this case should only include the successful transaction - with zip_file.open(f'financial-summary-{date_range}.csv') as f: + with zip_file.open(f'financial-summary-{report_window.display_text}.csv') as f: summary_content = f.read().decode('utf-8') self.assertEqual( 'Privileges purchased for Kentucky,1\n' @@ -1287,7 +1330,7 @@ def test_generate_report_does_not_include_transactions_with_declined_status(self ) # Check transaction detail, which in this case should only include the successful transaction - with zip_file.open(f'transaction-detail-{date_range}.csv') as f: + with zip_file.open(f'transaction-detail-{report_window.display_text}.csv') as f: detail_content = f.read().decode('utf-8') self.assertEqual( 'Licensee First Name,Licensee Last Name,Licensee Id,Transaction Settlement Date UTC,State,State Fee,Administrative Fee,Collected Transaction Fee,Transaction Id,Privilege Id,Transaction Status\n' diff --git a/backend/compact-connect/lambdas/python/purchases/tests/unit/test_data_model/__init__.py b/backend/compact-connect/lambdas/python/purchases/tests/unit/test_data_model/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py b/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py new file mode 100644 index 000000000..271cf5827 --- /dev/null +++ b/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py @@ -0,0 +1,214 @@ +from datetime import date, datetime +from unittest.mock import patch + +from tests import TstLambdas + + +class TestReportWindow(TstLambdas): + def test_weekly_report_window_calculated_ranges(self): + from report_window import ReportCycle, ReportWindow + + with ( + self.subTest('Monday morning'), + patch( + 'cc_common.config._Config.current_standard_datetime', + # A Monday morning, UTC + datetime.fromisoformat('2025-10-13T06:00:00+00:00'), + ), + ): + window = ReportWindow(ReportCycle.WEEKLY) + self.assertEqual( + window.query_window, + ( + # Saturdays at midnight + datetime.fromisoformat('2025-10-04T00:00:00+00:00'), + datetime.fromisoformat('2025-10-11T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # Saturday - Friday + date.fromisoformat('2025-10-04'), + date.fromisoformat('2025-10-10'), + ), + ) + + with ( + self.subTest('Saturday, midnight'), + patch( + 'cc_common.config._Config.current_standard_datetime', + # A Monday, midnight, UTC + datetime.fromisoformat('2025-10-11T00:00:00+00:00'), + ), + ): + window = ReportWindow(ReportCycle.WEEKLY) + self.assertEqual( + window.query_window, + ( + # Saturdays at midnight + datetime.fromisoformat('2025-10-04T00:00:00+00:00'), + datetime.fromisoformat('2025-10-11T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # Saturday - Friday + date.fromisoformat('2025-10-04'), + date.fromisoformat('2025-10-10'), + ), + ) + + with ( + self.subTest('Friday, very late'), + patch( + 'cc_common.config._Config.current_standard_datetime', + # Just before midnight, Friday night, UTC + datetime.fromisoformat('2025-10-10T23:59:59.999999+00:00'), + ), + ): + window = ReportWindow(ReportCycle.WEEKLY) + self.assertEqual( + window.query_window, + ( + # Saturdays at midnight, a week prior + datetime.fromisoformat('2025-09-27T00:00:00+00:00'), + datetime.fromisoformat('2025-10-04T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # Saturday - Friday + date.fromisoformat('2025-09-27'), + date.fromisoformat('2025-10-03'), + ), + ) + + def test_monthly_report_window(self): + from report_window import ReportCycle, ReportWindow + + with ( + self.subTest('Mid month'), + patch( + 'cc_common.config._Config.current_standard_datetime', + datetime.fromisoformat('2025-10-13T06:00:00+00:00'), + ), + ): + window = ReportWindow(ReportCycle.MONTHLY) + self.assertEqual( + window.query_window, + ( + # Beginning of each month + datetime.fromisoformat('2025-09-01T00:00:00+00:00'), + datetime.fromisoformat('2025-10-01T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # 1st - 30th + date.fromisoformat('2025-09-01'), + date.fromisoformat('2025-09-30'), + ), + ) + + with ( + self.subTest('Midnight on the 1st'), + patch( + 'cc_common.config._Config.current_standard_datetime', + datetime.fromisoformat('2025-10-01T00:00:00+00:00'), + ), + ): + window = ReportWindow(ReportCycle.MONTHLY) + self.assertEqual( + window.query_window, + ( + # First of each month + datetime.fromisoformat('2025-09-01T00:00:00+00:00'), + datetime.fromisoformat('2025-10-01T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # 1st - 30th + date.fromisoformat('2025-09-01'), + date.fromisoformat('2025-09-30'), + ), + ) + + with ( + self.subTest('30th, very late'), + patch( + 'cc_common.config._Config.current_standard_datetime', + # A Monday afternoon, UTC + datetime.fromisoformat('2025-09-30T23:59:59.999999+00:00'), + ), + ): + window = ReportWindow(ReportCycle.MONTHLY) + self.assertEqual( + window.query_window, + ( + # First of each month, a month ago + datetime.fromisoformat('2025-08-01T00:00:00+00:00'), + datetime.fromisoformat('2025-09-01T00:00:00+00:00'), + ), + ) + self.assertEqual( + window.display_window, + ( + # 1st - 31st + date.fromisoformat('2025-08-01'), + date.fromisoformat('2025-08-31'), + ), + ) + + def test_display_properties(self): + from report_window import ReportCycle, ReportWindow + + report_window = ReportWindow( + ReportCycle.WEEKLY, + _display_start_date=date.fromisoformat('2025-10-04'), + _display_end_date=date.fromisoformat('2025-10-10'), + ) + + self.assertEqual( + report_window.display_window, + ( + date.fromisoformat('2025-10-04'), + date.fromisoformat('2025-10-10'), + ), + ) + + self.assertEqual(report_window.display_start, date.fromisoformat('2025-10-04')) + self.assertEqual(report_window.display_start_text, '2025-10-04') + + self.assertEqual(report_window.display_end, date.fromisoformat('2025-10-10')) + self.assertEqual(report_window.display_end_text, '2025-10-10') + + self.assertEqual(report_window.display_text, '2025-10-04--2025-10-10') + + def test_query_properties(self): + from report_window import ReportCycle, ReportWindow + + report_window = ReportWindow( + ReportCycle.WEEKLY, + _display_start_date=date.fromisoformat('2025-10-04'), + _display_end_date=date.fromisoformat('2025-10-10'), + ) + + expected_start = datetime.fromisoformat('2025-10-04T00:00:00+00:00') + expected_end = datetime.fromisoformat('2025-10-11T00:00:00+00:00') + + self.assertEqual( + report_window.query_window, + ( + expected_start, + expected_end, + ), + ) + + self.assertEqual(report_window.start_epoch, int(expected_start.timestamp())) + self.assertEqual(report_window.end_epoch, int(expected_end.timestamp())) From 8c8584052a9cc66906af550e83fe74438962b3fd Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Fri, 17 Oct 2025 10:05:37 -0600 Subject: [PATCH 2/4] Shift report generation times back --- .../python/purchases/handlers/transaction_reporting.py | 2 +- backend/compact-connect/stacks/reporting_stack.py | 4 ++-- .../transaction_history_processing_workflow.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py index 0d91978bd..6c07c7f75 100644 --- a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py +++ b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py @@ -13,7 +13,7 @@ from cc_common.data_model.schema.compact.common import COMPACT_TYPE from cc_common.data_model.schema.jurisdiction.common import JURISDICTION_TYPE from cc_common.exceptions import CCInternalException -from lambdas.python.purchases.report_window import ReportCycle, ReportWindow +from report_window import ReportCycle, ReportWindow class ReportableTransactionStatuses(StrEnum): diff --git a/backend/compact-connect/stacks/reporting_stack.py b/backend/compact-connect/stacks/reporting_stack.py index 3eb42228e..8d3c04c3d 100644 --- a/backend/compact-connect/stacks/reporting_stack.py +++ b/backend/compact-connect/stacks/reporting_stack.py @@ -183,7 +183,7 @@ def _add_transaction_reporting_chain(self, persistent_stack: ps.PersistentStack) self, f'{compact.capitalize()}-WeeklyTransactionReportRule', # Send weekly reports every Friday at 10:00 PM UTC - schedule=Schedule.cron(week_day='FRI', hour='22', minute='0', month='*', year='*'), + schedule=Schedule.cron(week_day='SUN', hour='2', minute='0', month='*', year='*'), targets=[ LambdaFunction( handler=self.transaction_reporter, @@ -198,7 +198,7 @@ def _add_transaction_reporting_chain(self, persistent_stack: ps.PersistentStack) Rule( self, f'{compact.capitalize()}-MonthlyTransactionReportRule', - schedule=Schedule.cron(day='1', hour='8', minute='0', month='*', year='*'), + schedule=Schedule.cron(day='3', hour='2', minute='0', month='*', year='*'), targets=[ LambdaFunction( handler=self.transaction_reporter, diff --git a/backend/compact-connect/stacks/transaction_monitoring_stack/transaction_history_processing_workflow.py b/backend/compact-connect/stacks/transaction_monitoring_stack/transaction_history_processing_workflow.py index 7c2d1bfc2..35794bc4a 100644 --- a/backend/compact-connect/stacks/transaction_monitoring_stack/transaction_history_processing_workflow.py +++ b/backend/compact-connect/stacks/transaction_monitoring_stack/transaction_history_processing_workflow.py @@ -3,8 +3,8 @@ import os from aws_cdk import ArnFormat, Duration -from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, TreatMissingData # noqa: F401 temporarily unused -from aws_cdk.aws_cloudwatch_actions import SnsAction # noqa: F401 temporarily unused +from aws_cdk.aws_cloudwatch import Alarm, ComparisonOperator, TreatMissingData +from aws_cdk.aws_cloudwatch_actions import SnsAction from aws_cdk.aws_events import Rule, Schedule from aws_cdk.aws_events_targets import SfnStateMachine from aws_cdk.aws_iam import Effect, PolicyStatement From 40dd8c4f4a879a64a2d8c1a51ef43c7019ab1fcd Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Fri, 17 Oct 2025 13:40:14 -0600 Subject: [PATCH 3/4] Fix old comment --- backend/compact-connect/stacks/reporting_stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/stacks/reporting_stack.py b/backend/compact-connect/stacks/reporting_stack.py index 8d3c04c3d..1de33ea54 100644 --- a/backend/compact-connect/stacks/reporting_stack.py +++ b/backend/compact-connect/stacks/reporting_stack.py @@ -182,7 +182,7 @@ def _add_transaction_reporting_chain(self, persistent_stack: ps.PersistentStack) Rule( self, f'{compact.capitalize()}-WeeklyTransactionReportRule', - # Send weekly reports every Friday at 10:00 PM UTC + # Send weekly reports every Sunday at 2:00 AM UTC schedule=Schedule.cron(week_day='SUN', hour='2', minute='0', month='*', year='*'), targets=[ LambdaFunction( From 35ecb3bef15814b8329253e2c96e0d2c00a28f32 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Fri, 17 Oct 2025 15:53:08 -0600 Subject: [PATCH 4/4] PR feedback --- .../handlers/transaction_reporting.py | 4 ++-- .../lambdas/python/purchases/report_window.py | 12 +++++----- .../test_transaction_reporting.py | 24 +++++++++---------- .../tests/unit/test_report_window.py | 12 ++++------ 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py index 6c07c7f75..c3c969e19 100644 --- a/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py +++ b/backend/compact-connect/lambdas/python/purchases/handlers/transaction_reporting.py @@ -129,8 +129,8 @@ def generate_transaction_reports(event: dict, context: LambdaContext) -> dict: if report_start_override and report_end_override: report_window = ReportWindow( reporting_cycle, - _display_start_date=date.fromisoformat(report_start_override), - _display_end_date=date.fromisoformat(report_end_override), + display_start_date=date.fromisoformat(report_start_override), + display_end_date=date.fromisoformat(report_end_override), ) else: report_window = ReportWindow(reporting_cycle) diff --git a/backend/compact-connect/lambdas/python/purchases/report_window.py b/backend/compact-connect/lambdas/python/purchases/report_window.py index b529560ab..0d9404154 100644 --- a/backend/compact-connect/lambdas/python/purchases/report_window.py +++ b/backend/compact-connect/lambdas/python/purchases/report_window.py @@ -18,21 +18,21 @@ class ReportWindow: All windows start and end at midnight. """ - def __init__(self, report_cycle: ReportCycle, *, _display_start_date: date = None, _display_end_date: date = None): + def __init__(self, report_cycle: ReportCycle, *, display_start_date: date = None, display_end_date: date = None): """ :param report_cycle: The ReportCycle this report will run for (weekly or monthly) - :param _display_start_date: Optional override of start date. Required with _display_end_date. - :param _display_end_date: Optional override of end date. Required with _display_start_date. + :param display_start_date: Optional override of start date. Required with display_end_date. + :param display_end_date: Optional override of end date. Required with display_start_date. """ super().__init__() self._report_cycle = report_cycle - if _display_start_date and _display_end_date: + if display_start_date and display_end_date: self._start_time = datetime.combine( - _display_start_date, time(hour=0, minute=0, second=0, microsecond=0, tzinfo=UTC) + display_start_date, time(hour=0, minute=0, second=0, microsecond=0, tzinfo=UTC) ) self._end_time = datetime.combine( # Add 1 to convert display day to query datetime - _display_end_date + timedelta(days=1), + display_end_date + timedelta(days=1), time(hour=0, minute=0, second=0, microsecond=0, tzinfo=UTC), ) else: diff --git a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py index 104ce7933..1ed5b2b9e 100644 --- a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py +++ b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_transaction_reporting.py @@ -301,7 +301,7 @@ def test_generate_transaction_reports_sends_csv_with_zero_values_when_no_transac end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) # Generate the reports generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -389,7 +389,7 @@ def test_generate_transaction_reports_supports_date_overrides(self, mock_email_s end_date = date.fromisoformat('2025-03-28') # the start time should be the prior Saturday start_date = date.fromisoformat('2025-03-22') - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) # Just check the expected emails were 'sent' with dates older than default self._validate_compact_email_notification( @@ -440,7 +440,7 @@ def test_generate_report_collects_transactions_across_two_months(self, mock_emai end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -542,7 +542,7 @@ def test_generate_report_with_multiple_privileges_in_single_transaction(self, mo end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -648,7 +648,7 @@ def test_generate_report_with_large_number_of_transactions_and_providers(self, m end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -779,7 +779,7 @@ def test_generate_report_handles_unknown_jurisdiction(self, mock_email_service_c end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) self._add_compact_configuration_data(jurisdictions=[OHIO_JURISDICTION, KENTUCKY_JURISDICTION]) @@ -902,7 +902,7 @@ def test_generate_monthly_report_includes_expected_settled_transactions_for_full # the start time should be the first day of the montn display_start = date.fromisoformat('2024-02-01') report_window = ReportWindow( - ReportCycle.WEEKLY, _display_start_date=display_start, _display_end_date=display_end + ReportCycle.WEEKLY, display_start_date=display_start, display_end_date=display_end ) generate_transaction_reports(generate_mock_event(reporting_cycle=ReportCycle.MONTHLY), self.mock_context) @@ -1003,7 +1003,7 @@ def test_generate_weekly_report_includes_expected_settled_transactions_for_full_ end_date = date.fromisoformat('2025-03-07') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) generate_transaction_reports(generate_mock_event(reporting_cycle=ReportCycle.WEEKLY), self.mock_context) @@ -1065,7 +1065,7 @@ def test_generate_report_with_licensee_transaction_fees(self, mock_email_service end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -1141,7 +1141,7 @@ def test_generate_report_accounts_for_unknown_line_item_fees(self, mock_email_se end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) with self.assertRaises(CCInternalException) as exc_info: generate_transaction_reports(generate_mock_event(), self.mock_context) @@ -1224,7 +1224,7 @@ def test_generate_report_does_not_include_transactions_with_settlement_errors(se end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' @@ -1300,7 +1300,7 @@ def test_generate_report_does_not_include_transactions_with_declined_status(self end_date = date.fromisoformat('2025-04-04') # the start time should be the prior Saturday start_date = end_date - timedelta(days=6) - report_window = ReportWindow(ReportCycle.WEEKLY, _display_start_date=start_date, _display_end_date=end_date) + report_window = ReportWindow(ReportCycle.WEEKLY, display_start_date=start_date, display_end_date=end_date) expected_compact_path = ( f'compact/{TEST_COMPACT}/reports/compact-transactions/reporting-cycle/weekly/' diff --git a/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py b/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py index 271cf5827..b71736ce1 100644 --- a/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py +++ b/backend/compact-connect/lambdas/python/purchases/tests/unit/test_report_window.py @@ -12,7 +12,6 @@ def test_weekly_report_window_calculated_ranges(self): self.subTest('Monday morning'), patch( 'cc_common.config._Config.current_standard_datetime', - # A Monday morning, UTC datetime.fromisoformat('2025-10-13T06:00:00+00:00'), ), ): @@ -38,7 +37,6 @@ def test_weekly_report_window_calculated_ranges(self): self.subTest('Saturday, midnight'), patch( 'cc_common.config._Config.current_standard_datetime', - # A Monday, midnight, UTC datetime.fromisoformat('2025-10-11T00:00:00+00:00'), ), ): @@ -64,7 +62,6 @@ def test_weekly_report_window_calculated_ranges(self): self.subTest('Friday, very late'), patch( 'cc_common.config._Config.current_standard_datetime', - # Just before midnight, Friday night, UTC datetime.fromisoformat('2025-10-10T23:59:59.999999+00:00'), ), ): @@ -143,7 +140,6 @@ def test_monthly_report_window(self): self.subTest('30th, very late'), patch( 'cc_common.config._Config.current_standard_datetime', - # A Monday afternoon, UTC datetime.fromisoformat('2025-09-30T23:59:59.999999+00:00'), ), ): @@ -170,8 +166,8 @@ def test_display_properties(self): report_window = ReportWindow( ReportCycle.WEEKLY, - _display_start_date=date.fromisoformat('2025-10-04'), - _display_end_date=date.fromisoformat('2025-10-10'), + display_start_date=date.fromisoformat('2025-10-04'), + display_end_date=date.fromisoformat('2025-10-10'), ) self.assertEqual( @@ -195,8 +191,8 @@ def test_query_properties(self): report_window = ReportWindow( ReportCycle.WEEKLY, - _display_start_date=date.fromisoformat('2025-10-04'), - _display_end_date=date.fromisoformat('2025-10-10'), + display_start_date=date.fromisoformat('2025-10-04'), + display_end_date=date.fromisoformat('2025-10-10'), ) expected_start = datetime.fromisoformat('2025-10-04T00:00:00+00:00')