Fix GST reconciliation credential vault integration

This commit is contained in:
A R R R Associates
2026-09-10 21:59:20 +05:30
parent 477ea79f76
commit 90d7769bee
2 changed files with 98 additions and 11 deletions
@@ -66,18 +66,77 @@ def _is_gstin(reg, typ) -> bool:
return code == "GSTIN" and len(num) == 15
def _vault_entries(db, user, request, tenant_id: int, client_id: int, registration_id: int | None = None):
def _norm_gstin(value: str | None) -> str:
return re.sub(r"[^0-9A-Z]", "", str(value or "").upper())
def _credential_is_gst_portal(entry: CredentialVaultEntry) -> bool:
category = str(entry.category or "").strip().lower()
title = str(entry.title or "").strip().lower()
portal_url = str(entry.portal_url or "").strip().lower()
reference = str(entry.reference_number or "").strip().lower()
if category == "gst_portal":
return True
# Older/client-level vault entries may have been saved under a generic category.
# Accept them only when the entry itself clearly identifies a GST portal login.
haystack = " ".join((title, portal_url, reference))
gst_hint = (
"gst portal" in haystack
or "gst login" in haystack
or "services.gst.gov.in" in portal_url
or "www.gst.gov.in" in portal_url
)
if not gst_hint:
return False
# Do not silently substitute E-Way Bill, E-Invoice or API/provider secrets for
# a GST portal username/password unless the title explicitly says GST Portal/Login.
if category in {"eway_bill", "einvoice", "api_key"}:
return "gst portal" in title or "gst login" in title
return True
def _credential_matches_gstin(entry: CredentialVaultEntry, registration_id: int, gstin: str) -> bool:
if int(entry.registration_id or 0) == int(registration_id):
return True
if _norm_gstin(entry.reference_number) == _norm_gstin(gstin):
return True
# Client-level GST Portal credentials created before registration-level vault
# linking remain valid candidates for the selected client's GST registration.
return entry.registration_id is None
def _vault_entries(db, user, request, tenant_id: int, client_id: int, registration_id: int | None = None, gstin: str = ""):
q = select(CredentialVaultEntry).where(
CredentialVaultEntry.tenant_id == tenant_id,
CredentialVaultEntry.client_id == client_id,
CredentialVaultEntry.status != "archived",
)
if registration_id:
q = q.where(CredentialVaultEntry.registration_id == registration_id)
rows = db.execute(q.order_by(CredentialVaultEntry.title.asc())).scalars().all()
visible = [r for r in rows if can_view_entry(db, user, r, getattr(user, "branch_id", None))]
gst = [r for r in visible if "gst" in (str(r.title or "") + " " + str(r.category or "") + " " + str(r.portal_url or "")).lower()]
return gst or visible
branch_id = request.session.get("active_branch_id") or getattr(user, "branch_id", None)
visible = [r for r in rows if can_view_entry(db, user, r, int(branch_id) if branch_id else None)]
if not registration_id:
return [r for r in visible if _credential_is_gst_portal(r)]
eligible = [
r for r in visible
if _credential_is_gst_portal(r) and _credential_matches_gstin(r, int(registration_id), gstin)
]
def rank(entry: CredentialVaultEntry):
exact_registration = int(entry.registration_id or 0) == int(registration_id)
exact_gstin = _norm_gstin(entry.reference_number) == _norm_gstin(gstin)
client_level = entry.registration_id is None
return (
0 if exact_registration else 1 if exact_gstin else 2 if client_level else 3,
str(entry.title or "").lower(),
int(entry.id or 0),
)
return sorted(eligible, key=rank)
def _storage_payload(client, fy: str, gstin: str) -> tuple[str, str]:
@@ -109,7 +168,11 @@ def page(request: Request, client_id: int | None = None, registration_id: int |
selected_reg=next((r for r,t in registrations if registration_id and int(r.id)==int(registration_id)),None)
if not selected_reg and registrations:
selected_reg=registrations[0][0]
credentials=_vault_entries(db,user,request,scope.tenant_id,selected.id,int(selected_reg.id) if selected_reg else None)
selected_gstin = _norm_gstin(getattr(selected_reg, "registration_number", "")) if selected_reg else ""
credentials=_vault_entries(
db,user,request,scope.tenant_id,selected.id,
int(selected_reg.id) if selected_reg else None, selected_gstin,
)
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if selected_reg and period and node and _node_online(node):
gstin=re.sub(r"\s+","",str(selected_reg.registration_number or "").upper())
@@ -144,8 +207,17 @@ def start_download(request: Request, client_id: int=Form(...), registration_id:
if not pair: return _redirect(client_id,error="Select a valid GSTIN registration.")
reg,_=pair; gstin=re.sub(r"\s+","",str(reg.registration_number or "").upper())
cred=db.get(CredentialVaultEntry,credential_id)
if not cred or int(cred.client_id or 0)!=int(client.id) or (cred.registration_id and int(cred.registration_id)!=int(reg.id)) or not can_view_entry(db,user,cred,scope.branch_id):
return _redirect(client_id,registration_id=registration_id,period=period,error="Selected GST credential is not available for this client/registration.")
credential_ok = bool(
cred
and int(cred.tenant_id or 0) == int(scope.tenant_id)
and int(cred.client_id or 0) == int(client.id)
and str(cred.status or "").lower() != "archived"
and can_view_entry(db,user,cred,scope.branch_id)
and _credential_is_gst_portal(cred)
and _credential_matches_gstin(cred,int(reg.id),gstin)
)
if not credential_ok:
return _redirect(client_id,registration_id=registration_id,period=period,error="Selected GST Portal credential is not available for this client/GSTIN.")
username=decrypt_value(cred.tenant_id,cred.username_encrypted) or ""; password=decrypt_value(cred.tenant_id,cred.secret_encrypted) or ""
if not username or not password: return _redirect(client_id,registration_id=registration_id,period=period,error="GST username/password is missing in Credential Vault.")
fy=_fy_for_period(period); accounting_dir,gst_dir=_storage_payload(client,fy,gstin)