|
"""
|
|
NSDL Sync scheduled job -- fires the daily trigger call to AWS Phase B's
|
|
/sez/deliver-pending endpoint for this site's own client code.
|
|
|
|
This deliberately does NOT go through the 'Canofy API Call' / 'API
|
|
Configuration - Canofy' automatic-dispatch flow (canofy_api_call.py's
|
|
after_insert() -> _process_api_call()) -- that controller only ever acts
|
|
when "EWAY" appears in the linked API config's name/service_name
|
|
(hardcoded in _process_api_call()), so it would silently do nothing for an
|
|
NSDL config, and making it dispatch a new integration type would mean
|
|
editing that shared controller. Per instruction, nothing already in the
|
|
app is to be changed.
|
|
|
|
Instead, this reuses the two things that ARE genuinely generic:
|
|
- frappe.integrations.utils.make_post_request -- the actual shared
|
|
outbound-HTTP-call helper (this is what e_way_bill_request.py's own
|
|
execute_masters_india_api() calls internally).
|
|
- The 'API Configuration - Canofy' doctype as a config store, read the
|
|
same way e_way_bill_request.py's get_api_config() does (reimplemented
|
|
locally below rather than imported, so this module has zero dependency
|
|
on e-way-bill-specific code).
|
|
|
|
This file creates the 'NSDL Sync Run' doc itself, BEFORE calling the AWS
|
|
endpoint, as the parent record (Build Spec Phase 7) -- Phase B's Lambda
|
|
then UPDATES that same doc (never creates a second one) via the
|
|
sync_run_name passed through in the request body.
|
|
|
|
The AWS call is invoked ASYNCHRONOUSLY (the API Gateway integration sets
|
|
X-Amz-Invocation-Type: Event) -- Phase B's real work (an NSDL Playwright
|
|
login + throttled PDF downloads) routinely runs well past API Gateway's
|
|
hard 29s synchronous integration cap, so this call only confirms AWS
|
|
accepted the trigger. It does NOT return delivery counts; those arrive
|
|
later when Phase B itself updates the NSDL Sync Run doc.
|
|
|
|
--- One-time setup required per site (MANUAL, not done by this job) ---
|
|
1. Create one "API Configuration - Canofy" record named exactly
|
|
NSDL_SYNC_API_CONFIG_NAME below (or change the constant to match
|
|
whatever name you give it), with:
|
|
url = the deployed SezApiDeliverPendingUrl template
|
|
output (from `sam deploy`'s Outputs, AWS side)
|
|
api_key_header_key = "x-api-key"
|
|
api_secret = the real API Gateway key value
|
|
(aws apigateway get-api-key --api-key <SezApiKeyId>
|
|
--include-value)
|
|
headers = {"X-Frappe-Client": "<this site's client_name>"}
|
|
-- must match the client_name key used in the AWS
|
|
Secrets Manager CLIENTS block, NOT necessarily this
|
|
site's display name. Deliberately stored here (the
|
|
doctype's existing "Additional Static Headers"
|
|
field) rather than in site_config.json, so the
|
|
entire integration -- endpoint, credentials, and
|
|
this site's own identity -- lives in one place a
|
|
System Manager can edit from the UI, with no
|
|
server/file access needed.
|
|
2. Register this job in hooks.py:
|
|
scheduler_events = {
|
|
"daily": [
|
|
"canofy_tcs.import_documents.nsdl_sync.nsdl_sync_scheduled_job.trigger_nsdl_sync"
|
|
]
|
|
}
|
|
(adjust the dotted path to wherever this file actually lands in the app --
|
|
"Import_documents" is the module named for the NSDL Sync Run doctype;
|
|
this job would naturally live alongside it.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import frappe
|
|
from frappe.integrations.utils import make_post_request
|
|
|
|
NSDL_SYNC_API_CONFIG_NAME = "NSDL SEZ DELIVER PENDING"
|
|
|
|
|
|
def trigger_nsdl_sync():
|
|
"""Entry point for hooks.py's scheduler_events. Each site only ever
|
|
triggers its own client's delivery, once per day."""
|
|
try:
|
|
api_config = _get_api_config(NSDL_SYNC_API_CONFIG_NAME)
|
|
except frappe.DoesNotExistError:
|
|
frappe.log_error(
|
|
title="NSDL Sync misconfigured",
|
|
message=f"'{NSDL_SYNC_API_CONFIG_NAME}' API Configuration - Canofy record not found "
|
|
f"-- cannot trigger NSDL sync.",
|
|
)
|
|
return
|
|
|
|
# client_name lives in this same record's "Additional Static Headers"
|
|
# field (see module docstring's setup step 1) rather than
|
|
# site_config.json -- one less place to configure, and editable from
|
|
# Frappe's own UI.
|
|
client_name = json.loads(api_config.get("headers") or "{}").get("X-Frappe-Client")
|
|
if not client_name:
|
|
frappe.log_error(
|
|
title="NSDL Sync misconfigured",
|
|
message=f"'{NSDL_SYNC_API_CONFIG_NAME}' API Configuration - Canofy record's Additional "
|
|
f"Static Headers is missing an 'X-Frappe-Client' entry -- cannot trigger NSDL sync.",
|
|
)
|
|
return
|
|
|
|
sync_run = frappe.get_doc(
|
|
{
|
|
"doctype": "NSDL Sync Run",
|
|
"run_date": frappe.utils.nowdate(),
|
|
"client_name": client_name,
|
|
"status": "Triggered",
|
|
}
|
|
)
|
|
sync_run.insert(ignore_permissions=True)
|
|
|
|
try:
|
|
headers = _get_request_headers(api_config, client_name)
|
|
|
|
# Async invocation (X-Amz-Invocation-Type: Event, set by the AWS-side
|
|
# API Gateway mapping) -- this call only confirms AWS ACCEPTED the
|
|
# trigger, it does not carry the actual delivery outcome. Phase B
|
|
# updates this same doc itself once it finishes (see
|
|
# update_audit_doc() on the AWS side); a failure to invoke at all
|
|
# is caught here and separately covered by the DLQ + CloudWatch
|
|
# alarm on the AWS side, per Blueprint SS11.
|
|
make_post_request(
|
|
api_config["url"],
|
|
headers=headers,
|
|
data=json.dumps({"sync_run_name": sync_run.name}),
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
sync_run.db_set({"status": "Failed", "error_message": error_msg})
|
|
frappe.log_error(title="NSDL Sync trigger failed", message=error_msg)
|
|
|
|
|
|
def _get_api_config(linked_api: str) -> dict:
|
|
"""Mirrors the existing get_api_config() convention in
|
|
canofy_tcs/e_way_bill/doctype/e_way_bill_request/e_way_bill_request.py,
|
|
reimplemented locally so this module has no import dependency on
|
|
e-way-bill-specific code."""
|
|
api_doc = frappe.get_doc("API Configuration - Canofy", linked_api)
|
|
return {
|
|
"url": api_doc.url,
|
|
"headers": api_doc.headers,
|
|
"api_key_header_key": api_doc.api_key_header_key,
|
|
"api_secret": api_doc.get_password("api_secret"),
|
|
}
|
|
|
|
|
|
def _get_request_headers(api_config: dict, client_name: str) -> dict:
|
|
headers = json.loads(api_config.get("headers") or "{}")
|
|
headers[api_config.get("api_key_header_key")] = api_config.get("api_secret")
|
|
# Already present in `headers` (that's where trigger_nsdl_sync just read
|
|
# client_name from) -- reassigned explicitly anyway so this function's
|
|
# output is correct even if ever called with a client_name from
|
|
# elsewhere.
|
|
headers["X-Frappe-Client"] = client_name
|
|
headers["Content-Type"] = "application/json"
|
|
return headers
|