33from boto3 .dynamodb .conditions import Key
44
55from cc_common .config import _Config , logger
6- from cc_common .data_model .schema .transaction .record import TransactionRecordSchema , UnsettledTransactionRecordSchema
6+ from cc_common .data_model .schema .transaction import TransactionData
7+ from cc_common .data_model .schema .transaction .record import UnsettledTransactionRecordSchema
78
89AUTHORIZE_DOT_NET_CLIENT_TYPE = 'authorize.net'
910
@@ -14,21 +15,18 @@ class TransactionClient:
1415 def __init__ (self , config : _Config ):
1516 self .config = config
1617
17- def store_transactions (self , transactions : list [dict ]) -> None :
18+ def store_transactions (self , transactions : list [TransactionData ]) -> None :
1819 """
1920 Store transaction records in DynamoDB.
2021
21- :param compact: The compact name
2222 :param transactions: List of transaction records to store
2323 """
2424 with self .config .transaction_history_table .batch_writer () as batch :
2525 for transaction in transactions :
2626 # Convert UTC timestamp to epoch for sorting
27- transaction_processor = transaction [ ' transactionProcessor' ]
27+ transaction_processor = transaction . transactionProcessor
2828 if transaction_processor == AUTHORIZE_DOT_NET_CLIENT_TYPE :
29- transaction_schema = TransactionRecordSchema ()
30-
31- serialized_record = transaction_schema .dump (transaction )
29+ serialized_record = transaction .serialize_to_database_record ()
3230 batch .put_item (Item = serialized_record )
3331 else :
3432 raise ValueError (f'Unsupported transaction processor: { transaction_processor } ' )
@@ -80,6 +78,49 @@ def get_transactions_in_range(self, compact: str, start_epoch: int, end_epoch: i
8078
8179 return all_items
8280
81+ def get_most_recent_transaction_for_compact (self , compact : str ) -> TransactionData :
82+ """
83+ Get the most recent transaction for a compact.
84+
85+ Starts by querying the current month's partition key (based on config.current_standard_datetime),
86+ then sequentially queries previous months until a record is found.
87+
88+ :param compact: The compact name
89+ :return: The most recent transaction for the compact
90+ :raises ValueError: If no transactions are found for the compact
91+ """
92+ # Start with the current month
93+ current_date = self .config .current_standard_datetime .replace (day = 1 )
94+ # During normal operations, the most recent transaction should be no more than two days old, if there were any
95+ # transactions in that period. We'll look back up to three months, which should cover most reasonable
96+ # situations.
97+ max_months_to_check = 3
98+
99+ for _ in range (max_months_to_check ):
100+ month_key = current_date .strftime ('%Y-%m' )
101+ pk = f'COMPACT#{ compact } #TRANSACTIONS#MONTH#{ month_key } '
102+
103+ # Query for the most recent transaction in this month (descending order, limit 1)
104+ response = self .config .transaction_history_table .query (
105+ KeyConditionExpression = Key ('pk' ).eq (pk ),
106+ ScanIndexForward = False , # Descending order (most recent first)
107+ Limit = 1 ,
108+ )
109+
110+ items = response .get ('Items' , [])
111+ if items :
112+ # Found a transaction, return it
113+ return TransactionData .from_database_record (items [0 ])
114+
115+ # Move to previous month
116+ if current_date .month == 1 :
117+ current_date = current_date .replace (year = current_date .year - 1 , month = 12 )
118+ else :
119+ current_date = current_date .replace (month = current_date .month - 1 )
120+
121+ # No transactions found after checking max_months_to_check months
122+ raise ValueError (f'No transactions found for compact: { compact } ' )
123+
83124 def _query_transactions_for_month (
84125 self ,
85126 compact : str ,
@@ -122,10 +163,13 @@ def _query_transactions_for_month(
122163
123164 def _set_privilege_id_in_line_item (self , line_items : list [dict ], item_id_prefix : str , privilege_id : str ):
124165 for line_item in line_items :
125- if line_item .get ('itemId' ).lower ().startswith (item_id_prefix .lower ()):
166+ item_id = line_item .get ('itemId' )
167+ if item_id and item_id .lower ().startswith (item_id_prefix .lower ()):
126168 line_item ['privilegeId' ] = privilege_id
127169
128- def add_privilege_information_to_transactions (self , compact : str , transactions : list [dict ]) -> list [dict ]:
170+ def add_privilege_information_to_transactions (
171+ self , compact : str , transactions : list [TransactionData ]
172+ ) -> list [TransactionData ]:
129173 """
130174 Add privilege and licensee IDs to transaction line items based on the jurisdiction they were purchased for.
131175
@@ -134,7 +178,7 @@ def add_privilege_information_to_transactions(self, compact: str, transactions:
134178 :return: Modified list of transactions with privilege and licensee IDs added to line items
135179 """
136180 for transaction in transactions :
137- line_items = transaction [ ' lineItems' ]
181+ line_items = transaction . lineItems
138182 # Extract jurisdictions from line items with format priv:{compact}-{jurisdiction}-{license type abbr}
139183 jurisdictions_to_process = set ()
140184 for line_item in line_items :
@@ -145,7 +189,7 @@ def add_privilege_information_to_transactions(self, compact: str, transactions:
145189 jurisdictions_to_process .add (jurisdiction )
146190
147191 # Query for privilege records using the GSI
148- gsi_pk = f'COMPACT#{ compact } #TX#{ transaction [ " transactionId" ] } #'
192+ gsi_pk = f'COMPACT#{ compact } #TX#{ transaction . transactionId } #'
149193 response = self .config .provider_table .query (
150194 IndexName = self .config .compact_transaction_id_gsi_name ,
151195 KeyConditionExpression = Key ('compactTransactionIdGSIPK' ).eq (gsi_pk ),
@@ -157,9 +201,9 @@ def add_privilege_information_to_transactions(self, compact: str, transactions:
157201 logger .error (
158202 'No privilege records found for this transaction id.' ,
159203 compact = compact ,
160- transaction_id = transaction [ ' transactionId' ] ,
204+ transaction_id = transaction . transactionId ,
161205 # attempt to grab the licensee id from the authorize.net data, which may be invalid if it was masked
162- licensee_id = transaction [ ' licenseeId' ] ,
206+ licensee_id = transaction . licenseeId ,
163207 )
164208 # We mark the data as UNKNOWN so it still shows up in the history,
165209 # and move onto the next transaction
@@ -182,16 +226,16 @@ def add_privilege_information_to_transactions(self, compact: str, transactions:
182226 logger .error (
183227 'More than one matching provider id found for a transaction id.' ,
184228 compact = compact ,
185- transaction_id = transaction [ ' transactionId' ] ,
229+ transaction_id = transaction . transactionId ,
186230 # attempt to grab the licensee id from the authorize.net data, which may be invalid if it was masked
187- provider_ids = transaction [ ' licenseeId' ] ,
231+ provider_ids = transaction . licenseeId ,
188232 )
189233
190234 # The licensee id recorded in Authorize.net cannot be trusted, as Authorize.net masks any values that look
191235 # like a credit card number (consecutive digits separated by dashes). We need to grab the provider id from
192236 # the privileges associated with this transaction and set the licensee id on the transaction to that value
193237 # to ensure it is valid.
194- transaction [ 'licenseeId' ] = provider_ids .pop ()
238+ transaction . update ({ 'licenseeId' : provider_ids .pop ()} )
195239
196240 # Process each privilege record
197241 for jurisdiction in jurisdictions_to_process :
@@ -220,15 +264,16 @@ def add_privilege_information_to_transactions(self, compact: str, transactions:
220264 'No matching jurisdiction privilege record found for transaction. '
221265 'Cannot determine privilege id for this transaction' ,
222266 compact = compact ,
223- transactionId = transaction [ ' transactionId' ] ,
267+ transactionId = transaction . transactionId ,
224268 jurisdiction = jurisdiction ,
225- provider_id = transaction [ ' licenseeId' ] ,
269+ provider_id = transaction . licenseeId ,
226270 matching_privilege_records = response .get ('Items' , []),
227271 )
228272 # we set the privilege id to UNKNOWN, so that it will be visible in the report
229273 self ._set_privilege_id_in_line_item (
230274 line_items = line_items , item_id_prefix = item_id_prefix , privilege_id = 'UNKNOWN'
231275 )
276+ transaction .update ({'lineItems' : line_items })
232277
233278 return transactions
234279
@@ -269,7 +314,7 @@ def store_unsettled_transaction(self, compact: str, transaction_id: str, transac
269314 error = str (e ),
270315 )
271316
272- def reconcile_unsettled_transactions (self , compact : str , settled_transactions : list [dict ]) -> list [str ]:
317+ def reconcile_unsettled_transactions (self , compact : str , settled_transactions : list [TransactionData ]) -> list [str ]:
273318 """
274319 Reconcile unsettled transactions with settled transactions and detect old unsettled transactions.
275320
@@ -297,7 +342,7 @@ def reconcile_unsettled_transactions(self, compact: str, settled_transactions: l
297342 return []
298343
299344 # Create a set of settled transaction IDs for efficient lookup
300- settled_transaction_ids = {tx [ ' transactionId' ] for tx in settled_transactions }
345+ settled_transaction_ids = {tx . transactionId for tx in settled_transactions }
301346
302347 # Separate matched and unmatched unsettled transactions
303348 matched_unsettled = []
@@ -325,6 +370,16 @@ def reconcile_unsettled_transactions(self, compact: str, settled_transactions: l
325370 cutoff_time = datetime .now (UTC ) - timedelta (hours = 48 )
326371 old_unsettled_transactions = []
327372
373+ # We expect that all transactions we process from Authorize.net will match a record we have already
374+ # created at the time of purchase, as an unsettled transaction. Any mismatch is an error.
375+ matched_unsettled_transaction_ids = {tx ['transactionId' ] for tx in matched_unsettled }
376+ unmatched_settled_transaction_ids = settled_transaction_ids - matched_unsettled_transaction_ids
377+ if unmatched_settled_transaction_ids :
378+ logger .error (
379+ 'Unable to reconcile some transactions from Authorize.Net with our unsettled transactions' ,
380+ unreconciled_transactions = unmatched_settled_transaction_ids
381+ )
382+
328383 for unsettled_tx in unmatched_unsettled :
329384 transaction_date = datetime .fromisoformat (unsettled_tx ['transactionDate' ])
330385 if transaction_date < cutoff_time :
0 commit comments