@@ -219,22 +219,65 @@ def transmit_edocument(edocument_name):
219219 frappe .throw (_ ("Transmission failed: {0}" ).format (str (e )))
220220
221221
222+ def _handle_recommand_notification (notification : dict ) -> dict :
223+ """
224+ Handle Recommand webhook notification by fetching actual XML from API.
225+
226+ Args:
227+ notification: Recommand webhook payload with eventType, documentId, teamId
228+
229+ Returns:
230+ dict with xml_bytes and document_id, or raises exception on error
231+ """
232+ document_id = notification .get ("documentId" )
233+ team_id = notification .get ("teamId" )
234+
235+ if not document_id or not team_id :
236+ raise ValueError (f"Missing documentId or teamId in notification: { notification } " )
237+
238+ # Find integration settings by team_id (account_id)
239+ settings = frappe .db .get_value (
240+ "EDocument Integration Settings" ,
241+ {"account_id" : team_id , "edocument_integrator" : "Recommand" },
242+ ["name" , "edocument_profile" , "company" ],
243+ as_dict = True ,
244+ )
245+
246+ if not settings :
247+ raise ValueError (f"No Recommand integration settings found for team_id: { team_id } " )
248+
249+ # Get full integration settings with decrypted credentials
250+ integration_settings = get_edocument_integration_settings (settings .edocument_profile , settings .company )
251+
252+ # Fetch actual XML from Recommand API
253+ from .recommand_api import get_recommand_client
254+
255+ client = get_recommand_client (integration_settings )
256+ doc_details = client .get_document_status (team_id , document_id )
257+
258+ xml_content = doc_details .get ("document" , {}).get ("xml" )
259+ if not xml_content :
260+ raise ValueError (f"No XML content in document { document_id } . Response: { doc_details } " )
261+
262+ xml_bytes = xml_content .encode ("utf-8" ) if isinstance (xml_content , str ) else xml_content
263+ return {"xml_bytes" : xml_bytes , "document_id" : document_id }
264+
265+
266+ # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
222267@frappe .whitelist (allow_guest = True )
223268def webhook (** kwargs ):
224- # Webhook endpoint to receive incoming PEPPOL documents from providers
269+ """Webhook endpoint to receive incoming PEPPOL documents from providers."""
270+ import json
271+
225272 request_log = None
226273 try :
227274 r = frappe .request
228275 if not r :
229- frappe .log_error ("No request data received" , "E-Document Webhook Error" )
230276 return {"status" : "error" , "message" : "No request data received" }, 400
231277
232- xml_bytes = r .get_data ()
233- if not xml_bytes :
234- frappe .log_error ("No XML content found in request" , "E-Document Webhook Error" )
235- return {"status" : "error" , "message" : "No XML content found in request" }, 400
236- if isinstance (xml_bytes , str ):
237- xml_bytes = xml_bytes .encode ("utf-8" )
278+ request_data = r .get_data ()
279+ if not request_data :
280+ return {"status" : "error" , "message" : "No content found in request" }, 400
238281
239282 from frappe .integrations .utils import create_request_log
240283
@@ -245,50 +288,67 @@ def webhook(**kwargs):
245288 request_headers = r .headers ,
246289 )
247290
248- # Create EDocument record and attach XML file (no validation triggered)
249- edocument = frappe .get_doc (
250- {
251- "doctype" : "EDocument" ,
252- }
253- )
291+ # Try to parse as Recommand JSON notification, otherwise treat as raw XML
292+ xml_bytes = None
293+ document_id = None
294+
295+ try :
296+ data_str = request_data .decode ("utf-8" ) if isinstance (request_data , bytes ) else request_data
297+ notification = json .loads (data_str )
298+
299+ if notification .get ("eventType" ) == "document.received" :
300+ result = _handle_recommand_notification (notification )
301+ xml_bytes = result ["xml_bytes" ]
302+ document_id = result ["document_id" ]
303+ except (json .JSONDecodeError , UnicodeDecodeError ):
304+ pass # Not JSON, treat as raw XML
305+ except ValueError as e :
306+ frappe .log_error (str (e ), "E-Document Webhook Error" )
307+ return {"status" : "error" , "message" : str (e )}, 400
308+
309+ # Fallback: treat as raw XML
310+ if xml_bytes is None :
311+ xml_bytes = request_data .encode ("utf-8" ) if isinstance (request_data , str ) else request_data
312+
313+ # Check for duplicate
314+ if document_id :
315+ existing = frappe .db .exists ("EDocument" , {"reference" : document_id })
316+ if existing :
317+ result = {"edocument" : existing , "skipped" : True , "reason" : "duplicate" }
318+ request_log .status = "Completed"
319+ request_log .response = frappe .as_json (result )
320+ return {"status" : "success" , "result" : result }, 200
321+
322+ # Create EDocument and attach XML
323+ edocument = frappe .get_doc ({"doctype" : "EDocument" , "reference" : document_id })
254324 edocument .insert (ignore_permissions = True )
255- # Manual commit required: Webhook must persist EDocument before returning response to external service
256- frappe .db .commit () # nosemgrep
325+ frappe .db .commit () # nosemgrep: Webhook must persist before returning
257326
258- # Attach XML file
259- filename = f"document_{ edocument .name } .xml"
260327 file_doc = frappe .get_doc (
261328 {
262329 "doctype" : "File" ,
263- "file_name" : filename ,
330+ "file_name" : f"document_ { document_id or edocument . name } .xml" ,
264331 "attached_to_doctype" : "EDocument" ,
265332 "attached_to_name" : edocument .name ,
266333 "content" : xml_bytes ,
267334 "is_private" : 1 ,
268335 }
269336 )
270337 file_doc .save (ignore_permissions = True )
271- # Manual commit required: Webhook must persist File attachment before returning response to external service
272- frappe .db .commit () # nosemgrep
273-
274- result = {
275- "edocument" : edocument .name ,
276- }
338+ frappe .db .commit () # nosemgrep: Webhook must persist before returning
277339
340+ result = {"edocument" : edocument .name , "document_id" : document_id }
278341 request_log .status = "Completed"
279342 request_log .response = frappe .as_json (result )
280343 return {"status" : "success" , "result" : result }, 200
344+
281345 except Exception as e :
282346 if request_log :
283347 request_log .status = "Failed"
284348 request_log .error = frappe .get_traceback ()
285349 frappe .db .rollback ()
286- frappe .log_error (
287- f"E-Document webhook processing failed: { e !s} \n { frappe .get_traceback ()} " ,
288- "E-Document Webhook Error" ,
289- )
290- # Manual commit required: Webhook must commit error state before returning error response to external service
291- frappe .db .commit () # nosemgrep
350+ frappe .log_error (f"E-Document webhook failed: { e !s} " , "E-Document Webhook Error" )
351+ frappe .db .commit () # nosemgrep: Commit error state before returning
292352 return {"status" : "error" , "message" : "Internal server error" }, 500
293353 finally :
294354 if request_log :
0 commit comments