1818)
1919from aleph .toolkit .timestamp import timestamp_to_datetime , utc_now
2020from aleph .types .db_session import DbSession
21+ from aleph .types .sort_order import SortByCreditHistory , SortOrder
2122
2223
2324def _apply_credit_precision_multiplier (
@@ -869,6 +870,63 @@ def validate_credit_transfer_balance(
869870 return current_balance >= total_transfer_amount
870871
871872
873+ CREDIT_HISTORY_SORT_COLUMN_MAP = {
874+ SortByCreditHistory .MESSAGE_TIMESTAMP : AlephCreditHistoryDb .message_timestamp ,
875+ SortByCreditHistory .EXPIRATION_DATE : AlephCreditHistoryDb .expiration_date ,
876+ SortByCreditHistory .PAYMENT_METHOD : AlephCreditHistoryDb .payment_method ,
877+ SortByCreditHistory .AMOUNT : AlephCreditHistoryDb .amount ,
878+ SortByCreditHistory .ORIGIN : AlephCreditHistoryDb .origin ,
879+ SortByCreditHistory .TX_HASH : AlephCreditHistoryDb .tx_hash ,
880+ SortByCreditHistory .PROVIDER : AlephCreditHistoryDb .provider ,
881+ }
882+
883+ # Columns that are nullable and need NULLS LAST handling
884+ _NULLABLE_SORT_COLUMNS = {
885+ SortByCreditHistory .EXPIRATION_DATE ,
886+ SortByCreditHistory .ORIGIN ,
887+ SortByCreditHistory .TX_HASH ,
888+ SortByCreditHistory .PROVIDER ,
889+ }
890+
891+
892+ def _apply_credit_history_filters (
893+ query : Select ,
894+ tx_hash : Optional [str ] = None ,
895+ token : Optional [str ] = None ,
896+ chain : Optional [str ] = None ,
897+ provider : Optional [str ] = None ,
898+ origin : Optional [str ] = None ,
899+ origin_ref : Optional [str ] = None ,
900+ payment_method : Optional [str ] = None ,
901+ has_expiration : Optional [bool ] = None ,
902+ exclude_payment_method : Optional [List [str ]] = None ,
903+ ) -> Select :
904+ """Apply common filters to a credit history query."""
905+ if tx_hash is not None :
906+ query = query .where (AlephCreditHistoryDb .tx_hash == tx_hash )
907+ if token is not None :
908+ query = query .where (AlephCreditHistoryDb .token == token )
909+ if chain is not None :
910+ query = query .where (AlephCreditHistoryDb .chain == chain )
911+ if provider is not None :
912+ query = query .where (AlephCreditHistoryDb .provider == provider )
913+ if origin is not None :
914+ query = query .where (AlephCreditHistoryDb .origin == origin )
915+ if origin_ref is not None :
916+ query = query .where (AlephCreditHistoryDb .origin_ref == origin_ref )
917+ if payment_method is not None :
918+ query = query .where (AlephCreditHistoryDb .payment_method == payment_method )
919+ if has_expiration is True :
920+ query = query .where (AlephCreditHistoryDb .expiration_date .isnot (None ))
921+ elif has_expiration is False :
922+ query = query .where (AlephCreditHistoryDb .expiration_date .is_ (None ))
923+ if exclude_payment_method :
924+ query = query .where (
925+ AlephCreditHistoryDb .payment_method .notin_ (exclude_payment_method )
926+ )
927+ return query
928+
929+
872930def get_address_credit_history (
873931 session : DbSession ,
874932 address : str ,
@@ -881,75 +939,122 @@ def get_address_credit_history(
881939 origin : Optional [str ] = None ,
882940 origin_ref : Optional [str ] = None ,
883941 payment_method : Optional [str ] = None ,
884- after_time : Optional [dt .datetime ] = None ,
942+ has_expiration : Optional [bool ] = None ,
943+ exclude_payment_method : Optional [List [str ]] = None ,
944+ sort_by : SortByCreditHistory = SortByCreditHistory .MESSAGE_TIMESTAMP ,
945+ sort_order : SortOrder = SortOrder .DESCENDING ,
946+ after_sort_value : Optional [Any ] = None ,
885947 after_credit_ref : Optional [str ] = None ,
886948 after_credit_index : Optional [int ] = None ,
887949 cursor_mode : bool = False ,
888950) -> Sequence [AlephCreditHistoryDb ]:
889951 """
890- Get paginated credit history entries for a specific address, ordered from newest to oldest .
952+ Get paginated credit history entries for a specific address.
891953
892- Args:
893- session: Database session
894- address: Address to get credit history for
895- page: Page number (starts at 1)
896- pagination: Number of entries per page (0 for all entries)
897- tx_hash: Filter by transaction hash
898- token: Filter by token
899- chain: Filter by chain
900- provider: Filter by provider
901- origin: Filter by origin
902- origin_ref: Filter by origin reference
903- payment_method: Filter by payment method
904- after_time: Cursor-based: only return entries older than this timestamp
905- after_credit_ref: Cursor-based: tiebreaker credit_ref for entries with the same timestamp
906- after_credit_index: Cursor-based: tiebreaker credit_index for entries with the same timestamp and credit_ref
907-
908- Returns:
909- List of credit history entries ordered by message_timestamp desc
954+ Supports dynamic sorting and cursor-based or page-based pagination.
910955 """
911- query = (
912- select (AlephCreditHistoryDb )
913- .where (AlephCreditHistoryDb .address == address )
914- .order_by (
915- AlephCreditHistoryDb .message_timestamp .desc (),
956+ query = select (AlephCreditHistoryDb ).where (AlephCreditHistoryDb .address == address )
957+
958+ # Dynamic ordering
959+ primary_col = CREDIT_HISTORY_SORT_COLUMN_MAP [sort_by ]
960+ is_desc = sort_order == SortOrder .DESCENDING
961+
962+ if sort_by in _NULLABLE_SORT_COLUMNS :
963+ if is_desc :
964+ order_primary = primary_col .desc ().nullslast ()
965+ else :
966+ order_primary = primary_col .asc ().nullslast ()
967+ else :
968+ order_primary = primary_col .desc () if is_desc else primary_col .asc ()
969+
970+ # Tiebreakers for stable pagination
971+ if is_desc :
972+ query = query .order_by (
973+ order_primary ,
916974 AlephCreditHistoryDb .credit_ref .desc (),
917975 AlephCreditHistoryDb .credit_index .desc (),
918976 )
919- )
977+ else :
978+ query = query .order_by (
979+ order_primary ,
980+ AlephCreditHistoryDb .credit_ref .asc (),
981+ AlephCreditHistoryDb .credit_index .asc (),
982+ )
920983
921984 # Apply filters
922- if tx_hash is not None :
923- query = query .where (AlephCreditHistoryDb .tx_hash == tx_hash )
924- if token is not None :
925- query = query .where (AlephCreditHistoryDb .token == token )
926- if chain is not None :
927- query = query .where (AlephCreditHistoryDb .chain == chain )
928- if provider is not None :
929- query = query .where (AlephCreditHistoryDb .provider == provider )
930- if origin is not None :
931- query = query .where (AlephCreditHistoryDb .origin == origin )
932- if origin_ref is not None :
933- query = query .where (AlephCreditHistoryDb .origin_ref == origin_ref )
934- if payment_method is not None :
935- query = query .where (AlephCreditHistoryDb .payment_method == payment_method )
985+ query = _apply_credit_history_filters (
986+ query ,
987+ tx_hash = tx_hash ,
988+ token = token ,
989+ chain = chain ,
990+ provider = provider ,
991+ origin = origin ,
992+ origin_ref = origin_ref ,
993+ payment_method = payment_method ,
994+ has_expiration = has_expiration ,
995+ exclude_payment_method = exclude_payment_method ,
996+ )
936997
937- if after_time is not None :
938- query = query .where (
939- (AlephCreditHistoryDb .message_timestamp < after_time )
940- | (
941- (AlephCreditHistoryDb .message_timestamp == after_time )
942- & (
943- (AlephCreditHistoryDb .credit_ref < after_credit_ref )
944- | (
945- (AlephCreditHistoryDb .credit_ref == after_credit_ref )
946- & (AlephCreditHistoryDb .credit_index < after_credit_index )
998+ # Cursor-based keyset pagination
999+ if after_credit_ref is not None :
1000+ if after_sort_value is None and sort_by in _NULLABLE_SORT_COLUMNS :
1001+ # Last entry had NULL sort value — only compare tiebreakers within NULL group
1002+ if is_desc :
1003+ query = query .where (
1004+ (primary_col .is_ (None ))
1005+ & (
1006+ (AlephCreditHistoryDb .credit_ref < after_credit_ref )
1007+ | (
1008+ (AlephCreditHistoryDb .credit_ref == after_credit_ref )
1009+ & (AlephCreditHistoryDb .credit_index < after_credit_index )
1010+ )
9471011 )
9481012 )
1013+ else :
1014+ query = query .where (
1015+ (primary_col .is_ (None ))
1016+ & (
1017+ (AlephCreditHistoryDb .credit_ref > after_credit_ref )
1018+ | (
1019+ (AlephCreditHistoryDb .credit_ref == after_credit_ref )
1020+ & (AlephCreditHistoryDb .credit_index > after_credit_index )
1021+ )
1022+ )
1023+ )
1024+ elif is_desc :
1025+ query = query .where (
1026+ (primary_col < after_sort_value )
1027+ | (
1028+ (primary_col == after_sort_value )
1029+ & (
1030+ (AlephCreditHistoryDb .credit_ref < after_credit_ref )
1031+ | (
1032+ (AlephCreditHistoryDb .credit_ref == after_credit_ref )
1033+ & (AlephCreditHistoryDb .credit_index < after_credit_index )
1034+ )
1035+ )
1036+ )
1037+ # NULLs come after all non-NULLs due to NULLS LAST ordering
1038+ | (primary_col .is_ (None ))
1039+ )
1040+ else :
1041+ query = query .where (
1042+ (primary_col > after_sort_value )
1043+ | (
1044+ (primary_col == after_sort_value )
1045+ & (
1046+ (AlephCreditHistoryDb .credit_ref > after_credit_ref )
1047+ | (
1048+ (AlephCreditHistoryDb .credit_ref == after_credit_ref )
1049+ & (AlephCreditHistoryDb .credit_index > after_credit_index )
1050+ )
1051+ )
1052+ )
1053+ # NULLs come after all non-NULLs due to NULLS LAST ordering
1054+ | (primary_col .is_ (None ))
9491055 )
950- )
9511056
952- if after_time is not None or cursor_mode :
1057+ if after_credit_ref is not None or cursor_mode :
9531058 if pagination > 0 :
9541059 query = query .limit (pagination + 1 )
9551060 elif pagination > 0 :
@@ -968,43 +1073,28 @@ def count_address_credit_history(
9681073 origin : Optional [str ] = None ,
9691074 origin_ref : Optional [str ] = None ,
9701075 payment_method : Optional [str ] = None ,
1076+ has_expiration : Optional [bool ] = None ,
1077+ exclude_payment_method : Optional [List [str ]] = None ,
9711078) -> int :
9721079 """
9731080 Count total credit history entries for a specific address with optional filters.
974-
975- Args:
976- session: Database session
977- address: Address to count credit history for
978- tx_hash: Filter by transaction hash
979- token: Filter by token
980- chain: Filter by chain
981- provider: Filter by provider
982- origin: Filter by origin
983- origin_ref: Filter by origin reference
984- payment_method: Filter by payment method
985-
986- Returns:
987- Total number of credit history entries for the address matching the filters
9881081 """
9891082 query = select (func .count (AlephCreditHistoryDb .credit_ref )).where (
9901083 AlephCreditHistoryDb .address == address
9911084 )
9921085
993- # Apply filters
994- if tx_hash is not None :
995- query = query .where (AlephCreditHistoryDb .tx_hash == tx_hash )
996- if token is not None :
997- query = query .where (AlephCreditHistoryDb .token == token )
998- if chain is not None :
999- query = query .where (AlephCreditHistoryDb .chain == chain )
1000- if provider is not None :
1001- query = query .where (AlephCreditHistoryDb .provider == provider )
1002- if origin is not None :
1003- query = query .where (AlephCreditHistoryDb .origin == origin )
1004- if origin_ref is not None :
1005- query = query .where (AlephCreditHistoryDb .origin_ref == origin_ref )
1006- if payment_method is not None :
1007- query = query .where (AlephCreditHistoryDb .payment_method == payment_method )
1086+ query = _apply_credit_history_filters (
1087+ query ,
1088+ tx_hash = tx_hash ,
1089+ token = token ,
1090+ chain = chain ,
1091+ provider = provider ,
1092+ origin = origin ,
1093+ origin_ref = origin_ref ,
1094+ payment_method = payment_method ,
1095+ has_expiration = has_expiration ,
1096+ exclude_payment_method = exclude_payment_method ,
1097+ )
10081098
10091099 return session .execute (query ).scalar_one ()
10101100
0 commit comments