Skip to content

Commit 4ed0106

Browse files
author
Preetam Biswal
committed
fix: handle Recommand JSON webhook notifications
Webhook now detects Recommand notifications (eventType: document.received) and fetches actual XML from Recommand API using documentId. - Parse incoming data as JSON first - If Recommand notification, fetch XML via get_document_status API - Check for duplicates using EDocument.reference field - Fall back to raw XML for other providers (existing behavior)
1 parent db0ead9 commit 4ed0106

1 file changed

Lines changed: 91 additions & 31 deletions

File tree

edocument_integration/api.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,65 @@ def transmit_edocument(edocument_name):
122122
frappe.throw(_("Transmission failed: {0}").format(str(e)))
123123

124124

125+
def _handle_recommand_notification(notification: dict) -> dict:
126+
"""
127+
Handle Recommand webhook notification by fetching actual XML from API.
128+
129+
Args:
130+
notification: Recommand webhook payload with eventType, documentId, teamId
131+
132+
Returns:
133+
dict with xml_bytes and document_id, or raises exception on error
134+
"""
135+
document_id = notification.get("documentId")
136+
team_id = notification.get("teamId")
137+
138+
if not document_id or not team_id:
139+
raise ValueError(f"Missing documentId or teamId in notification: {notification}")
140+
141+
# Find integration settings by team_id (account_id)
142+
settings = frappe.db.get_value(
143+
"EDocument Integration Settings",
144+
{"account_id": team_id, "edocument_integrator": "Recommand"},
145+
["name", "edocument_profile", "company"],
146+
as_dict=True,
147+
)
148+
149+
if not settings:
150+
raise ValueError(f"No Recommand integration settings found for team_id: {team_id}")
151+
152+
# Get full integration settings with decrypted credentials
153+
integration_settings = get_edocument_integration_settings(settings.edocument_profile, settings.company)
154+
155+
# Fetch actual XML from Recommand API
156+
from .recommand_api import get_recommand_client
157+
158+
client = get_recommand_client(integration_settings)
159+
doc_details = client.get_document_status(team_id, document_id)
160+
161+
xml_content = doc_details.get("document", {}).get("xml")
162+
if not xml_content:
163+
raise ValueError(f"No XML content in document {document_id}. Response: {doc_details}")
164+
165+
xml_bytes = xml_content.encode("utf-8") if isinstance(xml_content, str) else xml_content
166+
return {"xml_bytes": xml_bytes, "document_id": document_id}
167+
168+
169+
# nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
125170
@frappe.whitelist(allow_guest=True)
126171
def webhook(**kwargs):
127-
# Webhook endpoint to receive incoming PEPPOL documents from providers
172+
"""Webhook endpoint to receive incoming PEPPOL documents from providers."""
173+
import json
174+
128175
request_log = None
129176
try:
130177
r = frappe.request
131178
if not r:
132-
frappe.log_error("No request data received", "E-Document Webhook Error")
133179
return {"status": "error", "message": "No request data received"}, 400
134180

135-
xml_bytes = r.get_data()
136-
if not xml_bytes:
137-
frappe.log_error("No XML content found in request", "E-Document Webhook Error")
138-
return {"status": "error", "message": "No XML content found in request"}, 400
139-
if isinstance(xml_bytes, str):
140-
xml_bytes = xml_bytes.encode("utf-8")
181+
request_data = r.get_data()
182+
if not request_data:
183+
return {"status": "error", "message": "No content found in request"}, 400
141184

142185
from frappe.integrations.utils import create_request_log
143186

@@ -148,50 +191,67 @@ def webhook(**kwargs):
148191
request_headers=r.headers,
149192
)
150193

151-
# Create EDocument record and attach XML file (no validation triggered)
152-
edocument = frappe.get_doc(
153-
{
154-
"doctype": "EDocument",
155-
}
156-
)
194+
# Try to parse as Recommand JSON notification, otherwise treat as raw XML
195+
xml_bytes = None
196+
document_id = None
197+
198+
try:
199+
data_str = request_data.decode("utf-8") if isinstance(request_data, bytes) else request_data
200+
notification = json.loads(data_str)
201+
202+
if notification.get("eventType") == "document.received":
203+
result = _handle_recommand_notification(notification)
204+
xml_bytes = result["xml_bytes"]
205+
document_id = result["document_id"]
206+
except (json.JSONDecodeError, UnicodeDecodeError):
207+
pass # Not JSON, treat as raw XML
208+
except ValueError as e:
209+
frappe.log_error(str(e), "E-Document Webhook Error")
210+
return {"status": "error", "message": str(e)}, 400
211+
212+
# Fallback: treat as raw XML
213+
if xml_bytes is None:
214+
xml_bytes = request_data.encode("utf-8") if isinstance(request_data, str) else request_data
215+
216+
# Check for duplicate
217+
if document_id:
218+
existing = frappe.db.exists("EDocument", {"reference": document_id})
219+
if existing:
220+
result = {"edocument": existing, "skipped": True, "reason": "duplicate"}
221+
request_log.status = "Completed"
222+
request_log.response = frappe.as_json(result)
223+
return {"status": "success", "result": result}, 200
224+
225+
# Create EDocument and attach XML
226+
edocument = frappe.get_doc({"doctype": "EDocument", "reference": document_id})
157227
edocument.insert(ignore_permissions=True)
158-
# Manual commit required: Webhook must persist EDocument before returning response to external service
159-
frappe.db.commit() # nosemgrep
228+
frappe.db.commit() # nosemgrep: Webhook must persist before returning
160229

161-
# Attach XML file
162-
filename = f"document_{edocument.name}.xml"
163230
file_doc = frappe.get_doc(
164231
{
165232
"doctype": "File",
166-
"file_name": filename,
233+
"file_name": f"document_{document_id or edocument.name}.xml",
167234
"attached_to_doctype": "EDocument",
168235
"attached_to_name": edocument.name,
169236
"content": xml_bytes,
170237
"is_private": 1,
171238
}
172239
)
173240
file_doc.save(ignore_permissions=True)
174-
# Manual commit required: Webhook must persist File attachment before returning response to external service
175-
frappe.db.commit() # nosemgrep
176-
177-
result = {
178-
"edocument": edocument.name,
179-
}
241+
frappe.db.commit() # nosemgrep: Webhook must persist before returning
180242

243+
result = {"edocument": edocument.name, "document_id": document_id}
181244
request_log.status = "Completed"
182245
request_log.response = frappe.as_json(result)
183246
return {"status": "success", "result": result}, 200
247+
184248
except Exception as e:
185249
if request_log:
186250
request_log.status = "Failed"
187251
request_log.error = frappe.get_traceback()
188252
frappe.db.rollback()
189-
frappe.log_error(
190-
f"E-Document webhook processing failed: {e!s}\n{frappe.get_traceback()}",
191-
"E-Document Webhook Error",
192-
)
193-
# Manual commit required: Webhook must commit error state before returning error response to external service
194-
frappe.db.commit() # nosemgrep
253+
frappe.log_error(f"E-Document webhook failed: {e!s}", "E-Document Webhook Error")
254+
frappe.db.commit() # nosemgrep: Commit error state before returning
195255
return {"status": "error", "message": "Internal server error"}, 500
196256
finally:
197257
if request_log:

0 commit comments

Comments
 (0)