@@ -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 )
126171def 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