Skip to content

Commit 1ee1afe

Browse files
authored
Merge pull request #9 from prilk-consulting/fix/recommand-webhook-notification
Fix/recommand webhook notification
2 parents ad002cf + 4ed0106 commit 1ee1afe

2 files changed

Lines changed: 111 additions & 36 deletions

File tree

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,30 @@ This app extends the [edocument](https://github.com/prilk-consulting/edocument)
1111

1212
This app requires the `edocument` app to be installed first.
1313

14-
You can install this app using the [bench](https://github.com/frappe/bench) CLI:
14+
### Supported Versions
15+
16+
| Frappe/ERPNext | Branch | Python | Node.js |
17+
|----------------|--------|--------|---------|
18+
| v15 | `version-15` | 3.10 | 18 |
19+
| v16 | `develop` | 3.14 | 24 |
20+
21+
### Installation Steps
1522

1623
```bash
1724
cd $PATH_TO_YOUR_BENCH
18-
bench get-app https://github.com/prilk-consulting/edocument_integration --branch $MAJOR_VERSION
19-
bench install-app edocument_integration
20-
```
2125

22-
Please use a branch (`MAJOR_VERSION`) that matches the major version of ERPNext you are using. For example, `version-14` or `version-15`. If you are a developer contributing new features, you'll want to use the `develop` branch instead.
26+
# For Frappe/ERPNext v15
27+
bench get-app https://github.com/prilk-consulting/edocument --branch version-15
28+
bench get-app https://github.com/prilk-consulting/edocument_integration --branch version-15
29+
30+
# For Frappe/ERPNext v16 (develop)
31+
bench get-app https://github.com/prilk-consulting/edocument
32+
bench get-app https://github.com/prilk-consulting/edocument_integration
33+
34+
# Install on your site
35+
bench --site your-site install-app edocument
36+
bench --site your-site install-app edocument_integration
37+
```
2338

2439
## Setup
2540

edocument_integration/api.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
223268
def 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

Comments
 (0)