bugfix(BIL-02)
This commit is contained in:
parent
d9f437f63e
commit
08cb98cfba
1 changed files with 207 additions and 12 deletions
|
|
@ -1540,16 +1540,40 @@ class BillingObjects:
|
||||||
if not accountIds:
|
if not accountIds:
|
||||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||||
|
|
||||||
recordFilter: Dict[str, Any] = {"accountId": accountIds}
|
# Extract free-text search term and run a custom query that covers
|
||||||
if userId:
|
# enriched columns (mandateName, userName) and the numeric amount
|
||||||
recordFilter["createdByUserId"] = userId
|
# 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()
|
||||||
|
|
||||||
result = self.db.getRecordsetPaginated(
|
if searchTerm:
|
||||||
BillingTransaction,
|
searchResult = self._searchTransactionsPaginated(
|
||||||
pagination=mappedPagination,
|
allAccounts=allAccounts,
|
||||||
recordFilter=recordFilter,
|
accountIds=accountIds,
|
||||||
)
|
userId=userId,
|
||||||
pageItems = result.get("items", []) if isinstance(result, dict) else result.items
|
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
|
||||||
|
|
||||||
|
result = self.db.getRecordsetPaginated(
|
||||||
|
BillingTransaction,
|
||||||
|
pagination=mappedPagination,
|
||||||
|
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}
|
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
|
row["userName"] = userMap.get(txUserId, txUserId) if txUserId else None
|
||||||
enriched.append(row)
|
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)
|
return PaginatedResult(items=enriched, totalItems=totalItems, totalPages=totalPages)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in getTransactionsForMandatesPaginated: {e}")
|
logger.error(f"Error in getTransactionsForMandatesPaginated: {e}")
|
||||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
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(
|
def _buildScopeFilter(
|
||||||
self,
|
self,
|
||||||
mandateIds: Optional[List[str]],
|
mandateIds: Optional[List[str]],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue