bugfix(BIL-02)
This commit is contained in:
parent
d9f437f63e
commit
08cb98cfba
1 changed files with 207 additions and 12 deletions
|
|
@ -1540,6 +1540,28 @@ class BillingObjects:
|
|||
if not accountIds:
|
||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||
|
||||
# Extract free-text search term and run a custom query that covers
|
||||
# enriched columns (mandateName, userName) and the numeric amount
|
||||
# column. The generic SQL search only covers TEXT columns of the
|
||||
# BillingTransaction table, which excludes these fields.
|
||||
searchTerm: Optional[str] = None
|
||||
if mappedPagination and mappedPagination.filters:
|
||||
raw = mappedPagination.filters.get("search")
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
searchTerm = raw.strip()
|
||||
|
||||
if searchTerm:
|
||||
searchResult = self._searchTransactionsPaginated(
|
||||
allAccounts=allAccounts,
|
||||
accountIds=accountIds,
|
||||
userId=userId,
|
||||
searchTerm=searchTerm,
|
||||
pagination=mappedPagination,
|
||||
)
|
||||
pageItems = searchResult["items"]
|
||||
totalItems = searchResult["totalItems"]
|
||||
totalPages = searchResult["totalPages"]
|
||||
else:
|
||||
recordFilter: Dict[str, Any] = {"accountId": accountIds}
|
||||
if userId:
|
||||
recordFilter["createdByUserId"] = userId
|
||||
|
|
@ -1550,6 +1572,8 @@ class BillingObjects:
|
|||
recordFilter=recordFilter,
|
||||
)
|
||||
pageItems = result.get("items", []) if isinstance(result, dict) else result.items
|
||||
totalItems = result.get("totalItems", 0) if isinstance(result, dict) else result.totalItems
|
||||
totalPages = result.get("totalPages", 0) if isinstance(result, dict) else result.totalPages
|
||||
|
||||
accountMap = {a.get("id"): a for a in allAccounts}
|
||||
|
||||
|
|
@ -1592,15 +1616,186 @@ class BillingObjects:
|
|||
row["userName"] = userMap.get(txUserId, txUserId) if txUserId else None
|
||||
enriched.append(row)
|
||||
|
||||
totalItems = result.get("totalItems", 0) if isinstance(result, dict) else result.totalItems
|
||||
totalPages = result.get("totalPages", 0) if isinstance(result, dict) else result.totalPages
|
||||
|
||||
return PaginatedResult(items=enriched, totalItems=totalItems, totalPages=totalPages)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in getTransactionsForMandatesPaginated: {e}")
|
||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||
|
||||
def _searchTransactionsPaginated(
|
||||
self,
|
||||
allAccounts: List[Dict[str, Any]],
|
||||
accountIds: List[str],
|
||||
userId: Optional[str],
|
||||
searchTerm: str,
|
||||
pagination: PaginationParams,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Custom paginated search for BillingTransaction that also covers the
|
||||
enriched columns `mandateName` and `userName` as well as the numeric
|
||||
`amount` column. Resolves matching mandate/user IDs via the app DB
|
||||
first, then builds a single SQL query with OR-combined conditions.
|
||||
"""
|
||||
import math
|
||||
from modules.connectors.connectorDbPostgre import _get_model_fields, _parseRecordFields
|
||||
from modules.datamodels.datamodelUam import UserInDB
|
||||
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
||||
|
||||
table = BillingTransaction.__name__
|
||||
fields = _get_model_fields(BillingTransaction)
|
||||
pattern = f"%{searchTerm}%"
|
||||
|
||||
# Resolve matching user / mandate IDs via the app DB (which is separate
|
||||
# from the billing DB and hosts UserInDB / Mandate tables).
|
||||
matchingUserIds: List[str] = []
|
||||
matchingMandateIds: List[str] = []
|
||||
try:
|
||||
appInterface = getAppInterface(self.currentUser)
|
||||
appInterface.db._ensure_connection()
|
||||
with appInterface.db.connection.cursor() as cur:
|
||||
if appInterface.db._ensureTableExists(UserInDB):
|
||||
cur.execute(
|
||||
'SELECT "id" FROM "UserInDB" WHERE '
|
||||
'COALESCE("username", \'\') ILIKE %s OR '
|
||||
'COALESCE("fullName", \'\') ILIKE %s OR '
|
||||
'COALESCE("email", \'\') ILIKE %s',
|
||||
(pattern, pattern, pattern),
|
||||
)
|
||||
matchingUserIds = [r["id"] for r in cur.fetchall() if r.get("id")]
|
||||
|
||||
if appInterface.db._ensureTableExists(Mandate):
|
||||
cur.execute(
|
||||
'SELECT "id" FROM "Mandate" WHERE '
|
||||
'COALESCE("label", \'\') ILIKE %s OR '
|
||||
'COALESCE("name", \'\') ILIKE %s',
|
||||
(pattern, pattern),
|
||||
)
|
||||
matchingMandateIds = [r["id"] for r in cur.fetchall() if r.get("id")]
|
||||
except Exception as e:
|
||||
logger.warning(f"_searchTransactionsPaginated: user/mandate resolution failed: {e}")
|
||||
|
||||
matchingAccountIds = [
|
||||
a.get("id") for a in allAccounts
|
||||
if a.get("id") and a.get("mandateId") in set(matchingMandateIds)
|
||||
]
|
||||
|
||||
# Try to interpret the search term as a number for amount matching.
|
||||
amountVal: Optional[float] = None
|
||||
try:
|
||||
amountVal = float(searchTerm.replace(",", "."))
|
||||
except Exception:
|
||||
amountVal = None
|
||||
|
||||
whereParts: List[str] = ['"accountId" = ANY(%s)']
|
||||
whereValues: List[Any] = [accountIds]
|
||||
if userId:
|
||||
whereParts.append('"createdByUserId" = %s')
|
||||
whereValues.append(userId)
|
||||
|
||||
# Apply non-search filters from pagination (reuse existing builder for
|
||||
# everything except the `search` key which we handle explicitly).
|
||||
import copy
|
||||
paginationWithoutSearch = copy.deepcopy(pagination) if pagination else None
|
||||
if paginationWithoutSearch and paginationWithoutSearch.filters:
|
||||
paginationWithoutSearch.filters = {
|
||||
k: v for k, v in paginationWithoutSearch.filters.items() if k != "search"
|
||||
}
|
||||
|
||||
orParts: List[str] = []
|
||||
orValues: List[Any] = []
|
||||
|
||||
textCols = [c for c, t in fields.items() if t == "TEXT"]
|
||||
for col in textCols:
|
||||
orParts.append(f'COALESCE("{col}"::TEXT, \'\') ILIKE %s')
|
||||
orValues.append(pattern)
|
||||
|
||||
if matchingUserIds:
|
||||
orParts.append('"createdByUserId" = ANY(%s)')
|
||||
orValues.append(matchingUserIds)
|
||||
if matchingAccountIds:
|
||||
orParts.append('"accountId" = ANY(%s)')
|
||||
orValues.append(matchingAccountIds)
|
||||
|
||||
orParts.append('"amount"::TEXT ILIKE %s')
|
||||
orValues.append(pattern)
|
||||
if amountVal is not None:
|
||||
orParts.append('"amount" = %s')
|
||||
orValues.append(amountVal)
|
||||
|
||||
whereParts.append(f"({' OR '.join(orParts)})")
|
||||
whereValues.extend(orValues)
|
||||
|
||||
# Apply remaining structured filters via the generic helper by feeding
|
||||
# it a dummy pagination that does NOT include LIMIT/OFFSET. We only
|
||||
# need the WHERE contribution for the non-search filters here.
|
||||
extraWhere = ""
|
||||
extraValues: List[Any] = []
|
||||
if paginationWithoutSearch and paginationWithoutSearch.filters:
|
||||
try:
|
||||
fromPagination = copy.deepcopy(paginationWithoutSearch)
|
||||
fromPagination.sort = []
|
||||
fromPagination.page = 1
|
||||
fromPagination.pageSize = 1
|
||||
ew, _, _, values, _ = self.db._buildPaginationClauses(
|
||||
BillingTransaction, fromPagination, recordFilter=None
|
||||
)
|
||||
if ew:
|
||||
extraWhere = ew.replace(" WHERE ", " AND ", 1)
|
||||
extraValues = list(values)
|
||||
except Exception as e:
|
||||
logger.warning(f"_searchTransactionsPaginated: extra-filter build failed: {e}")
|
||||
|
||||
whereClause = " WHERE " + " AND ".join(whereParts) + extraWhere
|
||||
whereValues.extend(extraValues)
|
||||
|
||||
# Build ORDER BY from pagination.sort
|
||||
validColumns = set(fields.keys())
|
||||
orderParts: List[str] = []
|
||||
if pagination and pagination.sort:
|
||||
for sf in pagination.sort:
|
||||
sfField = sf.get("field") if isinstance(sf, dict) else getattr(sf, "field", None)
|
||||
sfDir = sf.get("direction", "asc") if isinstance(sf, dict) else getattr(sf, "direction", "asc")
|
||||
if sfField and sfField in validColumns:
|
||||
direction = "DESC" if str(sfDir).lower() == "desc" else "ASC"
|
||||
colType = fields.get(sfField, "TEXT")
|
||||
if colType == "BOOLEAN":
|
||||
orderParts.append(f'COALESCE("{sfField}", FALSE) {direction}')
|
||||
else:
|
||||
orderParts.append(f'"{sfField}" {direction} NULLS LAST')
|
||||
if not orderParts:
|
||||
orderParts.append('"id"')
|
||||
orderClause = " ORDER BY " + ", ".join(orderParts)
|
||||
|
||||
pageSize = pagination.pageSize if pagination else 50
|
||||
page = pagination.page if pagination else 1
|
||||
offset = (page - 1) * pageSize
|
||||
limitClause = f" LIMIT {pageSize} OFFSET {offset}"
|
||||
|
||||
try:
|
||||
self.db._ensure_connection()
|
||||
with self.db.connection.cursor() as cur:
|
||||
countSql = f'SELECT COUNT(*) FROM "{table}"{whereClause}'
|
||||
cur.execute(countSql, whereValues)
|
||||
totalItems = cur.fetchone()["count"]
|
||||
|
||||
dataSql = f'SELECT * FROM "{table}"{whereClause}{orderClause}{limitClause}'
|
||||
cur.execute(dataSql, whereValues)
|
||||
records = [dict(row) for row in cur.fetchall()]
|
||||
|
||||
for rec in records:
|
||||
_parseRecordFields(rec, fields, f"search table {table}")
|
||||
|
||||
totalPages = math.ceil(totalItems / pageSize) if totalItems > 0 else 0
|
||||
return {"items": records, "totalItems": totalItems, "totalPages": totalPages}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"_searchTransactionsPaginated SQL error: {e}", exc_info=True)
|
||||
try:
|
||||
self.db.connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return {"items": [], "totalItems": 0, "totalPages": 0}
|
||||
|
||||
def _buildScopeFilter(
|
||||
self,
|
||||
mandateIds: Optional[List[str]],
|
||||
|
|
|
|||
Loading…
Reference in a new issue